之前实现了Springboot之Security前后端分离登录 刚好这段时间有空,乘机整合下OAuth2。记录下当中遇到的问题和处理方式。
什么是OAuth2?
OAuth 2.0 的一个简单解释
具体代码实现
POM文件
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
授权服务器
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter {
private static final String CLIENT_ID = "client"; //客户端
private static final String CLIENT_SECRET = "123456"; //secret客户端安全码
private static final String GRANT_TYPE_PASSWORD = "password"; // 密码模式授权模式
private static final String AUTHORIZATION_CODE = "authorization_code"; //授权码模式 授权码模式使用到了回调地址,是最为复杂的方式,通常网站中经常出现的微博,qq第三方登录,都会采用这个形式。
private static final String REFRESH_TOKEN = "refresh_token"; //
private static final String IMPLICIT = "implicit"; //简化授权模式
private static final String GRANT_TYPE = "client_credentials"; //客户端模式
private static final String SCOPE_WEB = "web"; //授权范围 web端
private static final String SCOPE_IOS = "ios"; //授权范围 ios端
private static final String SCOPE_ANDROID = "android";
private static final String SCOPE_BOOT = "boot"; //授权范围 项目名称
private static final int ACCESS_TOKEN_VALIDITY_SECONDS = 30*24*60*60; //token 有效时间 一个月
private static final int REFRESH_TOKEN_VALIDITY_SECONDS = 30*24*60*60; //刷新token有效时间 一个月
/**
* 描述:注入密码加密编码器 进行密码加密
*/
@Autowired
BCryptPasswordEncoder passwordEncoder;
/**
* 描述:注入用户信息处理类 处理用户账号信息
*/
@Autowired
UserDetailsServiceImpl userDetailService;
/**
* 描述:注入token生成器 处理token的生成方式
*/
@Autowired
TokenStore tokenStore;
/**
* 描述: 注入AuthenticationManager管理器
*/
@Autowired
AuthenticationManager authenticationManager;
/**
* 描述: 注入jwtAccessTokenConverter 增强token
*/
@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
String secret = new BCryptPasswordEncoder().encode(CLIENT_SECRET); // 用 BCrypt 对密码编码
//配置客户端信息
clients.inMemory() // 使用in-memory存储
.withClient(CLIENT_ID) //client_id用来标识客户的Id
.authorizedGrantTypes(AUTHORIZATION_CODE,GRANT_TYPE, REFRESH_TOKEN,GRANT_TYPE_PASSWORD,IMPLICIT) //允许授权类型
.scopes(SCOPE_WEB,SCOPE_IOS,SCOPE_ANDROID,SCOPE_BOOT) //允许授权范围
.authorities("ROLE_CLIENT") //客户端可以使用的权限
.secret(secret) //secret客户端安全码
.autoApprove(true) // 为true 则不会被重定向到授权的页面,也不需要手动给请求授权,直接自动授权成功返回code
.accessTokenValiditySeconds(ACCESS_TOKEN_VALIDITY_SECONDS) //token 时间秒
.refreshTokenValiditySeconds(REFRESH_TOKEN_VALIDITY_SECONDS);//刷新token 时间 秒
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
// 允许表单登录
.allowFormAuthenticationForClients()
// 密码加密编码器
.passwordEncoder(passwordEncoder)
// 允许所有的checkToken请求
.checkTokenAccess("permitAll()");
}
/**
* 配置令牌
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore)
// 认证管理器 - 在密码模式必须配置
.authenticationManager(authenticationManager)
// 自定义校验用户service
.userDetailsService(userDetailService)
// 是否能重复使用 refresh_token
.reuseRefreshTokens(false);
// 设置令牌增强 JWT 转换
TokenEnhancerChain enhancer = new TokenEnhancerChain();
enhancer.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
endpoints.tokenEnhancer(enhancer);
}
}
资源服务器
@Configuration
@EnableResourceServer
public class OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
TokenStore tokenStore;
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and().csrf().disable();
// 配置不登录可以访问 - 放行路径配置
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
registry.antMatchers("/login","/oauth/**").permitAll();
registry.anyRequest().authenticated();
}
}
token处理
@Configuration
public class TokenConfig {
/** JWT密钥 */
private String signingKey = "fastboot";
/**
* JWT 令牌转换器
* @return
*/
@Bean("jwtAccessTokenConverter")
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwt = new JwtAccessTokenConverter(){
/**
* 用户信息JWT加密
*/
@Override
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
UserInfo user = (UserInfo) authentication.getUserAuthentication().getPrincipal();
Set<String> tokenScope = token.getScope();
String scopeTemp = " ";
if(tokenScope!=null&&tokenScope.size()>0){
scopeTemp=tokenScope.iterator().next();
}
String scope =scopeTemp;
//将额外的参数信息存入,用于生成token
Map<String, Object> data = new HashMap<String, Object>(4){{
put("userId", user.getUserId());
put("username", user.getUsername());
put("email", user.getEmail());
put("roleDtos",user.getRoleDtos());
put("nickName", user.getNickName());
put("authorities", user.getAuthorities());
put("scope",scope);
}};
//自定义TOKEN包含的信息
token.setAdditionalInformation(data);
return super.encode(accessToken, authentication);
}
/**
* 用户信息JWT
*/
@Override
protected Map<String, Object> decode(String token) {
//解析请求当中的token 可以在解析后的map当中获取到上面加密的数据信息
Map<String, Object> decode = super.decode(token);
Long userId = (Long)decode.get("userId");
String username = (String)decode.get("username");
String email = (String)decode.get("email");
String nickName = (String)decode.get("nickName");
String scope = (String)decode.get("scope");
List<GrantedAuthority> grantedAuthorityList=new ArrayList<>();
//注意这里获取到的权限 虽然数据库存的权限是 "sys:menu:add" 但是这里就变成了"{authority=sys:menu:add}" 所以使用@PreAuthorize("hasAuthority('{authority=sys:menu:add}')")
List<LinkedHashMap<String,String>> authorities =(List<LinkedHashMap<String,String>>) decode.get("authorities");
for (LinkedHashMap<String, String> authority : authorities) {
SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority.getOrDefault("authority", "N/A"));
grantedAuthorityList.add(grantedAuthority);
}
UserInfo userInfo =new UserInfo(username,"N/A",userId, grantedAuthorityList);
userInfo.setNickName(nickName);
userInfo.setEmail(email);
//需要将解析出来的用户存入全局当中,不然无法转换成自定义的user类
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userInfo,null, grantedAuthorityList);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
decode.put("user_name",userInfo);
return decode;
}
};
jwt.setSigningKey(signingKey);
return jwt;
}
/**
* 配置 token 如何生成
* 1. InMemoryTokenStore 基于内存存储
* 2. JdbcTokenStore 基于数据库存储
* 3. JwtTokenStore 使用 JWT 存储 该方式可以让资源服务器自己校验令牌的有效性而不必远程连接认证服务器再进行认证
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public String getSigningKey() {
return signingKey;
}
public void setSigningKey(String signingKey) {
this.signingKey = signingKey;
}
}
controller接口
@RestController
@RequestMapping("/oauth")
public class OauthController {
@Autowired
TokenEndpoint tokenEndpoint;
@PostMapping(value = "/token")
public ResultInfo<OAuth2AccessToken> token(Principal principal, @RequestParam Map<String, String> parameters) throws Exception {
ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.postAccessToken(principal, parameters);
OAuth2AccessToken token = accessToken.getBody();
// TODO 可以考虑将返回的TOKEN信息存入redis或者数据库
return ResultInfo.success(token);
}
@PostMapping("/t1")
@PreAuthorize("hasAuthority('{authority=sys:menu:add}')")
public String getDemo(String name){
if(SecurityContextHolder.getContext() == null) {
return null;
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserInfo userInfo = (UserInfo) authentication.getPrincipal();
// return ResultInfo.success(userInfo);
return userInfo.toString();
}
}
上面基本上就是完整的代码了。其他的类如:UserDetailsServiceImpl,UserInfo 略!!!
遇到的问题
1 token过期时间设置 可以在OAuth2ServerConfiguration 当中设置accessTokenValiditySeconds(秒) 也可以在TokenConfig 里面进行jwt加密的时候进行设置,token.setExpiration(); 设置后会覆盖OAuth2ServerConfiguration 当中的。
2 权限不匹配问题 虽然数据库存的权限是 "sys:menu:add" 但是oauth2取的时候变成了"{authority=sys:menu:add}" 所以使用接口上使用@PreAuthorize("hasAuthority('{authority=sys:menu:add}')")进行权限匹配。
3 serurity的User类无法转换为自定义的user子类的问题,需要在 JWT解密的时候,重新构建然后存入全局当中。(PS:无法在获取token当中获取到自定义的user子类)