前言
oauth2 规范中具备了四种授权模式,分别如下:
·授权码模式:authorization code
·简化模式:implicit
·密码模式:resource owner password credentials
·客户端模式:client credentials
注:本示例只演示密码模式,感兴趣的同学自己花时间测试另外三种授权模式。
配置 mongodb 和 jwt
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 表说明