oauth2规范中具备了四种授权模式,分别如下:
·授权码模式:authorization code
·简化模式:implicit
·密码模式:resource owner password credentials
·客户端模式:client credentials
注:本示例只演示密码模式,感兴趣的同学自己花时间测试另外三种授权模式。
1、新建Application入口应用类
@SpringBootApplication @RestController @EnableEurekaClient // 该服务将作为OAuth2服务 @EnableAuthorizationServer // 注意:不加@EnableResourceServer注解,下面user信息为空 @EnableResourceServer public class Application { @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @GetMapping("/user") public Map<String, Object> user(OAuth2Authentication user){ Map<String, Object> userInfo = new HashMap<>(); userInfo.put("user", user.getUserAuthentication().getPrincipal()); userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities())); return userInfo; } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
2、新建JWTOAuth2Config类
@Configuration public class JWTOAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private ClientDetailsService mongoClientDetailsService; @Autowired private UserDetailsService mongoUserDetailsService; @Autowired private TokenStore tokenStore; @Autowired private DefaultTokenServices tokenServices; // 将JWTTokenStore类中的JwtAccessTokenConverter关联到OAUTH2 @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter; // 自动将JWTTokenEnhancer装配到TokenEnhancer类中 // token增强类,需要添加额外信息内容的就用这个类 @Autowired private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { // /oauth/token // 如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走 // ClientCredentialsTokenEndpointFilter来保护 // 如果没有支持allowFormAuthenticationForClients或者有支持但是url中没有client_id和client_secret的,走basic认证保护 security.tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { // Spring Oauth 允许开发人员挂载多个令牌增强器,因此将令牌增强器添加到TokenEnhancerChain类中 // 设置jwt签名和jwt增强器到TokenEnhancerChain TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtTokenEnhancer, jwtAccessTokenConverter)); endpoints.tokenStore(tokenStore) // 在jwt和oauth2服务器之间充当翻译(签名) .accessTokenConverter(jwtAccessTokenConverter) // 令牌增强器类:扩展jwt token .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager) .userDetailsService(mongoUserDetailsService); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // 使用mongodb保存客户端信息 clients.withClientDetails(mongoClientDetailsService); } }
3、新建JWTTokenEnhancer令牌增强器类
// 令牌增强器类 public class JWTTokenEnhancer implements TokenEnhancer { // 要进行增强需要覆盖enhance方法 @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { Map<String, Object> additionalInfo = new HashMap<>(); String newContent ="这是新加的内容"; additionalInfo.put("newContent", newContent); // 所有附加的属性都放到HashMap中,并设置在传入该方法的accessToken变量上 ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo); return oAuth2AccessToken; } }
4、新建JWTTokenStoreConfig类以支持jwt
// 用于定义Spring将如何管理JWT令牌的创建、签名和翻译 @Configuration public class JWTTokenStoreConfig { @Autowired private ServiceConfig serviceConfig; // 设置TokenStore为JwtTokenStore @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtAccessTokenConverter()); } // @Primary作用:如果有多个特定类型bean那么就使用被@Primary标注的bean类型进行自动注入 @Bean @Primary public DefaultTokenServices tokenServices() { // 用于从出示给服务的令牌中读取数据 DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); defaultTokenServices.setTokenStore(tokenStore()); defaultTokenServices.setSupportRefreshToken(true); return defaultTokenServices; } // 在jwt和oauth2服务器之间充当翻译 @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); // 定义将用于签署令牌的签名密钥(自定义 存储在git上authentication.yml文件) // jwt是不保密的,所以要另外加签名验证jwt token converter.setSigningKey(serviceConfig.getJwtSigningKey()); return converter; } // 设置TokenEnhancer增强器中使用JWTTokenEnhancer增强器 @Bean public TokenEnhancer jwtTokenEnhancer() { return new JWTTokenEnhancer(); } }
5、新建WebSecurityConfigurer类,设置访问权限以及基本配置
@Configuration public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Autowired BCryptPasswordEncoder passwordEncoder; // 用来处理用户验证 // 被注入OAuth2Config类中的 endpoints方法中 @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } // Spring会自动寻找同样类型的具体类注入,这里就是MongoUserDetailsService了 @Autowired private UserDetailsService userDetailsService; // 定义用户、密码和用色的地方 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder); ; } //不加这段代码不显示返回的json信息 @Override protected void configure(HttpSecurity http) throws Exception { http // 设置成form登录,前端就要使用form-data传参 // .formLogin() // 设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参 .httpBasic() .and() .authorizeRequests() .antMatchers("/register").permitAll() .anyRequest() .authenticated() .and().csrf().disable().cors(); } }
6、新建MongoClientDetailsService类,校验及更新mongodb存储的客户端信息
@Service("mongoClientDetailsService") public class MongoClientDetailsService implements ClientDetailsService { private final String CONLLECTION_NAME = "oauth_client_details"; @Autowired MongoTemplate mongoTemplate; @Autowired BCryptPasswordEncoder passwordEncoder; // private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance(); public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { BaseClientDetails client = mongoTemplate.findOne(new Query(Criteria.where("clientId").is(clientId)), BaseClientDetails.class, CONLLECTION_NAME); if(client==null){ throw new RuntimeException("没有查询到客户端信息"); } return client; } public void addClientDetails(ClientDetails clientDetails) { mongoTemplate.insert(clientDetails, CONLLECTION_NAME); } public void updateClientDetails(ClientDetails clientDetails) { Update update = new Update(); update.set("resourceIds", clientDetails.getResourceIds()); update.set("clientSecret", clientDetails.getClientSecret()); update.set("authorizedGrantTypes", clientDetails.getAuthorizedGrantTypes()); update.set("registeredRedirectUris", clientDetails.getRegisteredRedirectUri()); update.set("authorities", clientDetails.getAuthorities()); update.set("accessTokenValiditySeconds", clientDetails.getAccessTokenValiditySeconds()); update.set("refreshTokenValiditySeconds", clientDetails.getRefreshTokenValiditySeconds()); update.set("additionalInformation", clientDetails.getAdditionalInformation()); update.set("scope", clientDetails.getScope()); mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientDetails.getClientId())), update, CONLLECTION_NAME); } public void updateClientSecret(String clientId, String secret) { Update update = new Update(); update.set("clientSecret", secret); mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientId)), update, CONLLECTION_NAME); } public void removeClientDetails(String clientId) { mongoTemplate.remove(new Query(Criteria.where("clientId").is(clientId)), CONLLECTION_NAME); } public List<ClientDetails> listClientDetails(){ return mongoTemplate.findAll(ClientDetails.class, CONLLECTION_NAME); } }
7、新建MongoUserDetailsService类,检验存储的用户数据
@Service public class MongoUserDetailsService implements UserDetailsService { private final String USER_CONLLECTION = "userAuth"; @Autowired MongoTemplate mongoTemplate; public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // identifier:1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博 UserAuth userAuth = mongoTemplate.findOne(new Query(Criteria.where("identifier").is(username)), UserAuth.class, USER_CONLLECTION); if(userAuth == null) { throw new RuntimeException("没有查询到用户信息"); } return new User(username, userAuth.getCertificate(), mapToGrantedAuthorities(userAuth.getRoles())); } private static List<GrantedAuthority> mapToGrantedAuthorities(List<String> authorities) { return authorities.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); } }
8、实体类
User.java
@Setter @Getter public class User { private String id; private String uid; //用户名 private String userName; //用户昵称 private String nickName; //是否是超级管理员 private boolean admin; // 性别 private String gender; // 生日 private Long birthday; //个性签名 private String signature; //email private String email; //email private Long emailBindTime; //mobile private String mobile; //mobile private Long mobileBindTime; // 头像 private String face; // 头像200*200 private String face200; // 原图图像 private String srcface; //状态 2正常用户 3禁言用户 4虚拟用户 5运营 private Integer status; // 类型 private Integer type; }
UserAuth.java
@Getter @Setter public class UserAuth { // id private String id; private String uid; // 1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博 private Integer identityType; // 手机号 邮箱 用户名或第三方应用的唯一标识 private String identifier; // 密码凭证(站内的保存密码,站外的不保存或保存token) private String certificate; // md5 盐值加密 private String md5; //角色ID private List<String> roles; }
9、表结构
oauth_client_details: { "_id" : ObjectId("5f01e1cf1315d14f5bea1679"), "clientId" : "core-resource", "resourceIds" : "card", "clientSecret" : "$2a$10$8NUXEVgWW72Gf.QQtQlsQu1L9KGxAonW.QfO3s82Kr9DADL4wn24K", "authorizedGrantTypes" : "password,authorization_code,refresh_token", "registeredRedirectUris" : "http://localhost:9001/base/login", "authorities" : "", "accessTokenValiditySeconds" : "7200", "refreshTokenValiditySeconds" : "0", "autoapprove" : true, "additionalInformation" : null, "scope" : "all" } user: { "_id" : ObjectId("5e7d56c9b03e9a046ab26cac"), "uid" : "5e7d56c9b03e9a046ab26ca9", "username" : "zhangwei", "admin" : false, "email" : "zhangwei900808@126.com", "emailBindTime" : NumberLong(1585272521646), "status" : 2, "type" : 1 } userAuth: { "_id" : ObjectId("5e7d56c9b03e9a046ab26caa"), "uid" : "5e7d56c9b03e9a046ab26ca9", "identityType" : 2, "identifier" : "zhangwei900808@126.com", "certificate" : "$2a$10$OdHuIooHSv60jC7YYahQB.k2JPUo3..Jdb0KwRcn9F9yrz64HPFfC", "roles" : [ "ROLE_USER" ] } { "_id" : ObjectId("5e7d56c9b03e9a046ab26cab"), "uid" : "5e7d56c9b03e9a046ab26ca9", "identityType" : 3, "identifier" : "zhangwei", "certificate" : "$2a$10$nHqwjbwAjgHeTu3.lunKVuVe6fa/7zcFZ6bVSrrkGkEZ7OIYOdkMe", "roles" : [ "ROLE_USER" ] }
1、oauth2保存客户端信息有好多种:内存,jdbc。像我这里使用的是mongodb
2、数据库表User保存的是用户基本信息,真正密码和访问类型(用户名、邮箱、手机号等等)是在表UserAuth里面,这个大家注意下
3、在WebSecurityConfigurerAdapter类中,设置成form登录,前端就要使用form-data传参,设置成basic登录,前端就可以使用application/x-www-form-urlencoded传参
4、AuthorizationServerConfigurerAdapter中要设置获取token的路由/oauth/token能被访问到,还要设置成下面这段代码:
@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients(); }
Spring Security Oauth2 授权服务开发之MongoDB
解决Spring Security OAuth在访问/oauth/token时候报401 authentication is required
Spring cloud oauth2 研究--oauth_client_detail表说明