公司项目调整, 想拥有一个自己的用户中心,类似盛大令牌这种。好家伙,需要一套自己的令牌,简单来说就是一个认证中心,负责颁发令牌。
因为暂时是内部系统使用,直接账号密码,手机号验证码,邮箱验证码登录等等。等自己项目做起来了第三方对接过来才考虑OAuth2协议。之前看过一个Sa-token的框架,写的挺简洁的,但是考虑了SpringSecurity还是稳(出了Bug好搜索)。最后决定使用SpringSecurityOAuth2。
使用jwt
主要代码()
1.1 UserDetailsService
这里存在一个大坑。
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//这里请求用户中心访问数据
return userService.getUserByName(username);
}
}
1.2 自定义的用户认证转换器(增强器)CustomUserAuthenticationConverter
@Component
public class CustomUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
@Resource UserDetailsService userDetailsService;
/**
* 拓展jwt
*
* @param authentication
* @return
*/
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
LinkedHashMap map = new LinkedHashMap();
String name = authentication.getName();
map.put("user_name", name);
Object principal = authentication.getPrincipal();
OAuthUserDetail userJwt = null;
if (principal instanceof OAuthUserDetail) {
userJwt = (OAuthUserDetail) principal;
} else {
// refresh_token默认不去调用userdetailService获取用户信息,这里我们手动去调用,得到 UserJwt
UserDetails userDetails = userDetailsService.loadUserByUsername(name);
userJwt = (OAuthUserDetail) userDetails;
}
map.put("phone", userJwt.getPhone());
map.put("uid", userJwt.getId());
map.put("appUserId", userJwt.getAppUserId());
// 时间戳
map.put("timestamp", System.currentTimeMillis());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
map.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return map;
}
}
1.3 OAuth2Config
/**
* @author:longcheng
* @date:2021/12/17 10:09
* @version:1.0
*/
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
// 数据库连接池对象
@Autowired
private DataSource dataSource;
// 认证业务对象
@Autowired
private UserDetailServiceImpl userService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private OAuthServerAuthenticationEntryPoint authServerAuthenticationEntryPoint;
@Autowired
private CustomUserAuthenticationConverter authenticationConverter;
@Autowired
private KeyProperties keyProperties;
// 客户端信息来源
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Bean
public TokenStore tokenStore() {
JwtTokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
// 授权码模式数据来源
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new JdbcAuthorizationCodeServices(dataSource);
}
// 授权信息保存策略
@Bean
public ApprovalStore approvalStore() {
return new JdbcApprovalStore(dataSource);
}
//
@Bean
public CustomDefaultTokenServices customTokenServices() {
CustomDefaultTokenServices tokenServices = new CustomDefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(true);
tokenServices.setAuthenticationManager(authenticationManager);
tokenServices.setTokenEnhancer(accessTokenConverter());
// access_token有效期:2个小时 -> 60*60*2
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 2);
// refresh_token有效期:12个小时 -> 60*60*12
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 12);
return tokenServices;
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair =
new KeyStoreKeyFactory(
keyProperties.getKeyStore().getLocation(),
keyProperties.getKeyStore().getSecret().toCharArray())
.getKeyPair(
keyProperties.getKeyStore().getAlias(),
keyProperties.getKeyStore().getPassword().toCharArray());
converter.setKeyPair(keyPair);
// 配置自定义的CustomUserAuthenticationConverter
DefaultAccessTokenConverter accessTokenConverter =
(DefaultAccessTokenConverter) converter.getAccessTokenConverter();
accessTokenConverter.setUserTokenConverter(authenticationConverter);
return converter;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// ClientId、Client-Secret:这两个参数对应请求端定义的 cleint-id 和 client-secret
clients.withClientDetails(jdbcClientDetailsService());
}
/**
* 主配置信息
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// 指定服务异常翻译器
.exceptionTranslator(new AuthResponseExceptionTranslator())
.approvalStore(approvalStore())
.authenticationManager(authenticationManager)
.userDetailsService(userService)
//授权码模式需要
.authorizationCodeServices(authorizationCodeServices())
// token 管理
.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
.tokenServices(customTokenServices());
}
// 检查token的策略
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
AuthClientCredentialsTokenEndpointFilter authClientCredentialsTokenEndpointFilter =
new AuthClientCredentialsTokenEndpointFilter(security, authServerAuthenticationEntryPoint);
authClientCredentialsTokenEndpointFilter.afterPropertiesSet();
security.addTokenEndpointAuthenticationFilter(authClientCredentialsTokenEndpointFilter);
// security.allowFormAuthenticationForClients();
// security.authenticationEntryPoint(authServerAuthenticationEntryPoint);
// 开启token认证权限
security.tokenKeyAccess("permitAll");
// 开启token校验
security.checkTokenAccess("permitAll");
}
}
1.4 异常翻译器
一看这么多if/else if。这里它自己也是这么写的。异常处理也有点麻烦,后续再说。
重写这个异常处理器是为了返回我自己要的格式。Result风格,根据code码来区分。
/**
* ClientCredentialsTokenEndpointFilter
*
* @author:longcheng
* @date:2021/12/20 13:54
* @version:1.0 重写服务异常翻译器
*/
public class AuthResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
@Override
public ResponseEntity translate(Exception e) throws Exception {
return new ResponseEntity(doTranslateHandler(e), HttpStatus.UNAUTHORIZED);
}
/**
* 错误类型
*
* @param e
* @return
*/
private R doTranslateHandler(Exception e) {
if (e instanceof InvalidClientException) {
return R.fail(AuthResultCode.INVALID_CLIENT);
} else if (e instanceof UnauthorizedClientException) {
return R.fail(AuthResultCode.UNAUTHORIZED_CLIENT);
} else if (e instanceof InvalidGrantException) {
return R.fail(AuthResultCode.INVALID_GRANT);
} else if (e instanceof InvalidScopeException) {
return R.fail(AuthResultCode.INVALID_SCOPE);
} else if (e instanceof InvalidTokenException) {
return R.fail(AuthResultCode.INVALID_TOKEN);
} else if (e instanceof InvalidRequestException) {
return R.fail(AuthResultCode.INVALID_REQUEST);
} else if (e instanceof RedirectMismatchException) {
return R.fail(AuthResultCode.REDIRECT_URI_MISMATCH);
} else if (e instanceof UnsupportedGrantTypeException) {
return R.fail(AuthResultCode.UNSUPPORTED_GRANT_TYPE);
} else if (e instanceof UnsupportedResponseTypeException) {
return R.fail(AuthResultCode.UNSUPPORTED_RESPONSE_TYPE);
} else if (e instanceof UserDeniedAuthorizationException) {
return R.fail(AuthResultCode.ACCESS_DENIED);
}
return R.fail(AuthResultCode.LOGIN_ERROR);
}
}
1.4 OauthController
@RestController
@RequestMapping("/oauth")
@Api(tags = "token认证获取")
public class OauthController {
@Autowired
private TokenEndpoint tokenEndpoint;
@Autowired
private CheckTokenEndpoint checkTokenEndpoint;
@PostMapping("/token")
@ApiOperation("Post请求token")
@ApiImplicitParams({
@ApiImplicitParam(name = "grant_type", value = "认证类型", dataType = "String", required = true),
@ApiImplicitParam(name = "client_id", value = "系统id", dataType = "String", required = true),
@ApiImplicitParam(name = "client_secret", value = "系统密钥", dataType = "String", required = true),
@ApiImplicitParam(name = "username", value = "账号", dataType = "String", required = false),
@ApiImplicitParam(name = "password", value = "密码", dataType = "String", required = false),
@ApiImplicitParam(name = "refresh_token", value = "刷新token", required = false),
})
public R<Object> postAccessToken(
Principal principal, @RequestParam Map<String, String> parameters)
throws HttpRequestMethodNotSupportedException {
return R.data(tokenEndpoint.postAccessToken(principal, parameters).getBody());
}
@ApiOperation("验证token")
@GetMapping("/check_token")
public R<Object> checkToken(@RequestParam("token") String value) {
return R.data(checkTokenEndpoint.checkToken(value));
}
}
好了,大功告成。。
具体测试那些,这个版本是写了很久了的,所以测试那些截图都没有了。
代码由于笔者之前太忙了,忘了测试和保存了。所以会存在一些问题,而且这些代码网上也很多,这里不详细解释了。后续更新自定义passwordToken和referToken。