讲义
地址
package com.hao.auth_demo.config;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.hao.auth_demo.constant.ApiConstant;
import com.hao.auth_demo.constant.Constant;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWT;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
/**
* 自定义 OAuth2 令牌服务
* 支持 JWT 令牌和非-JWT 令牌
*/
@Service
public class CustomResourceServerTokenServices implements ResourceServerTokenServices {
/**
* 根据令牌值加载身份验证信息
* @param accessTokenValue
* @return
* @throws AuthenticationException
* @throws InvalidTokenException
*/
@Override
public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException, InvalidTokenException {
if (accessTokenValue == null || accessTokenValue.isEmpty()) {
throw new InvalidTokenException("Invalid token");
}
// 根据令牌类型执行不同的验证和解析逻辑
if (isJWTToken(accessTokenValue)) {
// 如果是 JWT 令牌,调用 JWT 验证和解析逻辑
return validateAndParseJWT(accessTokenValue);
} else {
// 如果是非-JWT 令牌,调用 RemoteTokenServices 验证逻辑
return validateNonJWTToken(accessTokenValue);
}
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
/**
* 检查令牌是否为 JWT 令牌
* 供 loadAuthentication() 方法调用
* @param token
* @return
*/
private boolean isJWTToken(String token) {
if (token == null || token.isEmpty()) {
return false;
}
// 检查令牌是否包含点号
if (token.split("\\.").length < 3) {
return false;
}
// 尝试解码头部和载荷
try {
String[] parts = token.split("\\.");
String header = new String(Base64.getDecoder().decode(parts[0]));
String payload = new String(Base64.getDecoder().decode(parts[1]));
// 检查头部是否包含 alg 字段
if (header.contains("\"alg\":")) {
return true;
}
} catch (Exception e) {
// 解码失败,不是 JWT 令牌
}
return false;
}
/**
* 验证并解析 JWT 令牌
* 供 loadAuthentication() 方法调用
* @param jwtToken
* @return
*/
private OAuth2Authentication validateAndParseJWT(String jwtToken) {
// 实现 JWT 验证和解析逻辑
if (!verifySignature(jwtToken)) {
throw new InvalidTokenException("Invalid token");
}
try {
// 在这里解析JWT并从中提取有关身份验证的信息
JWT jwt = JWTParser.parse(jwtToken);
// 从JWT中提取必要的信息,例如身份信息、权限等
String subject = jwt.getJWTClaimsSet().getSubject();
// 取中间部分
String payload = jwtToken.split("\\.")[1];
// 解码
String json = new String(Base64.getDecoder().decode(payload));
// 转换成JSON对象
JSONObject jsonObject = JSON.parseObject(json);
// 获取用户信息
String name = jsonObject.getString("name");
String userName = jsonObject.getString("userName");
String phone = jsonObject.getString("mobile");
String email = jsonObject.getString("email");
// 创建用户对象
UserDetails userDetails = createUser(name, userName, phone, email);
// 创建一个包含身份信息的OAuth2Authentication对象
// 这只是一个示例,您需要根据实际需求构建OAuth2Authentication对象
// 在这里,您可以使用一些用户信息和权限来构建OAuth2Authentication对象
OAuth2Request oauth2Request = new OAuth2Request(null, Constant.CLIENT_ID, null, true, null, null, null, null, null);
List<GrantedAuthority> authorities = new ArrayList<>(); // 添加用户权限
Authentication user = new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
OAuth2Authentication authentication = new OAuth2Authentication(oauth2Request, user);
return authentication;
} catch (Exception e) {
// 在解析或处理JWT时出现错误
throw new InvalidTokenException("Invalid token");
}
}
/**
* 创建用户对象
* 供 validateAndParseJWT() 方法调用
* @param name
* @param userName
* @param phone
* @param email
* @return
*/
private static UserDetails createUser(String name, String userName, String phone, String email) {
// 在此处根据提取的信息创建用户对象,这只是一个示例
// 您可能需要实现一个自定义的 UserDetailsService 来从数据库或其他地方检索用户信息
// 并返回实现了 UserDetails 接口的用户对象
// 创建一个简单的 User 对象作为示例
User user = new User(userName, "", Collections.emptyList());
// 在这里可以设置用户的其他属性,例如姓名、电话和电子邮件
return user;
}
/**
* 验证非 JWT 令牌
* 供 loadAuthentication() 方法调用
* @param nonJWTToken
* @return
*/
private OAuth2Authentication validateNonJWTToken(String nonJWTToken) {
// 调用 RemoteTokenServices 进行令牌验证
// 使用 RemoteTokenServices 进行远程令牌验证
RemoteTokenServices remoteTokenServices = tokenServices();
OAuth2Authentication authentication = remoteTokenServices.loadAuthentication(nonJWTToken);
// 根据验证结果创建 OAuth2Authentication
if (authentication != null) {
// 根据需要,您可能还需要对 authentication 进行一些处理
return authentication;
} else {
throw new InvalidTokenException("Invalid token");
}
}
public RemoteTokenServices tokenServices(){
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(ApiConstant.API_CHECK_TOKEN);
remoteTokenServices.setClientId(Constant.CLIENT_ID);
remoteTokenServices.setClientSecret(Constant.CLIENT_SECRET);
return remoteTokenServices;
}
private List<RSAKey> getPublicKeys() throws ParseException {
// 读取认证中心JWT公钥列表
List<RSAKey> rsaKeys = new ArrayList<>();
HttpResponse execute = HttpUtil.createGet("https://dfcvtest.bccastle.com/api/v1/oauth2/keys").execute();
if (execute.getStatus() == 200){
String body = execute.body();
JSONObject bodyJSON = JSON.parseObject(body);
JSONArray keysJSON = JSONObject.parseArray(bodyJSON.getString("keys"));
for (Object key : keysJSON) {
String jsonObject = JSONObject.parseObject(key.toString()).toJSONString();
rsaKeys.add(RSAKey.parse(jsonObject));
}
return rsaKeys;
}else {
System.out.println("调用 /api/v1/oauth2/keys 获取公钥列表失败!");
return new ArrayList<>();
}
}
private boolean verifySignature(String id_token) {
try {
JWT jwtToken = JWTParser.parse(id_token);
SignedJWT jwt = (SignedJWT)jwtToken;
List<RSAKey> publicKeyList = getPublicKeys();
RSAKey rsaKey = null;
for (RSAKey key : publicKeyList) {
if (jwt.getHeader().getKeyID().equals(key.getKeyID())) {
rsaKey = key;
}
}
if (rsaKey != null) {
RSASSAVerifier verifier = new RSASSAVerifier(rsaKey.toRSAPublicKey());
return jwt.verify(verifier);
}else {
System.out.println("无法验证令牌签名");
return false;
}
} catch (Exception e) {
System.out.println("验证令牌签名失败!"+e.getMessage());
return false;
}
}
}
@Autowired
private ResourceServerTokenServices customResourceServerTokenServices;
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(customResourceServerTokenServices);
}
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
public void configure(ResourceServerSecurityConfigurer resources) {
resources.tokenStore(tokenStore)
.tokenServices(new CustomTokenService());
}
@Bean
public TokenStore tokenStore()
{
RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
tokenStore.setPrefix(CacheConstants.OAUTH_ACCESS);
// 解决每次生成的 token都一样的问题
tokenStore.setAuthenticationKeyGenerator(oAuth2Authentication -> UUID.randomUUID().toString());
return tokenStore;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
@Bean
@Primary
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenEnhancer(tokenEnhancer());
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(redisClientDetailsService);
return tokenServices;
}
package co.yixiang.common.security.config;
import co.yixiang.common.security.domain.JwtUser;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import java.util.HashMap;
import java.util.Map;
import static co.yixiang.common.core.constant.SecurityConstants.*;
/**
* token增强,客户端模式不增强。
*/
public class CustomTokenEnhancer implements TokenEnhancer {
/**
* Provides an opportunity for customization of an access token (e.g. through its
* additional information map) during the process of creating a new token for use by a
* client.
* @param accessToken the current access token with its expiration and refresh token
* @param authentication the current authentication including client and user details
* @return a new token enhanced with additional information
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
JwtUser user = (JwtUser) authentication.getPrincipal();
final Map<String, Object> additionalInfo = new HashMap<>();
Map<String, Object> userDetails = new HashMap<>();
userDetails.put(DETAILS_USER_ID, user.getId());
userDetails.put(DETAILS_USERNAME, user.getUsername());
additionalInfo.put(DETAILS_USER, user);
// Set additional information in token for retriving in #org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
@Autowired
private TokenStore tokenStore;
/**
* 退出登陆
* @param authHeader
* @return
*/
@DeleteMapping("/logout")
public ApiResult<?> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader)
{
if (StringUtils.isEmpty(authHeader))
{
return ApiResult.ok();
}
String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StringUtils.EMPTY).trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null || StringUtils.isEmpty(accessToken.getValue()))
{
return ApiResult.ok();
}
// 清空 access token
tokenStore.removeAccessToken(accessToken);
// 清空 refresh token
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
return ApiResult.ok();
}
package co.yixiang.common.security.service;
import co.yixiang.common.redis.service.RedisService;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 重写原生方法支持redis缓存
* @author yshop
*/
@Slf4j
@Service
public class RedisClientDetailsService extends JdbcClientDetailsService
{
/**
* 缓存 client的 redis key,这里是 hash结构存储
*/
private static final String CACHE_CLIENT_KEY = "client_details";
private final RedisService redisService;
public RedisClientDetailsService(DataSource dataSource, RedisService redisService) {
super(dataSource);
this.redisService = redisService;
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails clientDetails = null;
String value = (String) redisService.hget(CACHE_CLIENT_KEY, clientId);
if (StringUtils.isBlank(value)) {
clientDetails = cacheAndGetClient(clientId);
} else {
clientDetails = JSONObject.parseObject(value, BaseClientDetails.class);
}
return clientDetails;
}
/**
* 缓存 client并返回 client
*
* @param clientId clientId
*/
public ClientDetails cacheAndGetClient(String clientId) {
ClientDetails clientDetails = null;
clientDetails = super.loadClientByClientId(clientId);
if (clientDetails != null) {
BaseClientDetails baseClientDetails = (BaseClientDetails) clientDetails;
Set<String> autoApproveScopes = baseClientDetails.getAutoApproveScopes();
if (CollectionUtils.isNotEmpty(autoApproveScopes)) {
baseClientDetails.setAutoApproveScopes(
autoApproveScopes.stream().map(this::convert).collect(Collectors.toSet())
);
}
redisService.hset(CACHE_CLIENT_KEY, clientId, JSONObject.toJSONString(baseClientDetails));
}
return clientDetails;
}
/**
* 删除 redis缓存
*
* @param clientId clientId
*/
public void removeRedisCache(String clientId) {
redisService.hdel(CACHE_CLIENT_KEY, clientId);
}
/**
* 将 oauth_client_details全表刷入 redis
*/
public void loadAllClientToCache() {
if (redisService.hasKey(CACHE_CLIENT_KEY)) {
return;
}
log.info("将oauth_client_details全表刷入redis");
List<ClientDetails> list = super.listClientDetails();
if (CollectionUtils.isEmpty(list)) {
log.error("oauth_client_details表数据为空,请检查");
return;
}
list.forEach(client -> redisService.hset(CACHE_CLIENT_KEY, client.getClientId(), JSONObject.toJSONString(client)));
}
private String convert(String value) {
final String logicTrue = "1";
return logicTrue.equals(value) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
}
}
将原本clientDetailsService
替换为redisClientDetailsService
package co.yixiang.auth.handler;
import co.yixiang.common.core.api.ApiResult;
import co.yixiang.common.core.api.YshopException;
import co.yixiang.common.core.constant.SecurityConstants;
import co.yixiang.common.core.utils.ServletUtils;
import co.yixiang.common.core.utils.StringUtils;
import co.yixiang.common.redis.utils.RedisUtils;
import co.yixiang.common.security.service.RedisClientDetailsService;
import co.yixiang.common.security.token.DfPhoneAuthenticationToken;
import co.yixiang.weixin.common.utils.JsonUtils;
import org.apache.commons.collections.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.common.exceptions.UnapprovedClientAuthenticationException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Service;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author mk
* @version 1.0
* @description: TODO
* @date 2021/6/7 22:49
*/
@Service
public class DfPhoneLoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private RedisClientDetailsService redisClientDetailsService;
private final AuthorizationServerTokenServices tokenServices;
private final AuthenticationManager authenticationManager;
private final OAuth2RequestFactory oAuth2RequestFactory;
private final RedisUtils redisUtil;
private AuthorizationServerTokenServices authorizationServerTokenServices;
public DfPhoneLoginSuccessHandler(AuthorizationServerTokenServices tokenServices, AuthenticationManager authenticationManager, RedisUtils redisUtil,
OAuth2RequestFactory oAuth2RequestFactory, AuthorizationServerTokenServices authorizationServerTokenServices) {
this.tokenServices = tokenServices;
this.authenticationManager = authenticationManager;
this.oAuth2RequestFactory = oAuth2RequestFactory;
this.redisUtil=redisUtil;
this.authorizationServerTokenServices=authorizationServerTokenServices;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
final OAuth2AccessToken token = this.getOauth2AccessToken(request, authentication);
Map map=new HashMap<>();
map.put("access_token",token.getValue());
map.put("expired",token.getExpiration());
ApiResult<Map> ok = ApiResult.ok(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JsonUtils.toJson(ok));
}
private OAuth2AccessToken getOauth2AccessToken(HttpServletRequest request, Authentication authentication) {
request.setAttribute(SecurityConstants.LOGIN_TYPE, SecurityConstants.APP_LOGIN);
String loginClientId = "yshop";
ClientDetails clientDetails = null;
try {
clientDetails = redisClientDetailsService.loadClientByClientId(loginClientId);
} catch (Exception e) {
throw new YshopException("获取移动端登录可用的Client失败");
}
if (clientDetails == null) {
throw new YshopException("未找到移动端登录可用的Client");
}
String grantTypes = String.join(",", clientDetails.getAuthorizedGrantTypes());
Map<String, String> requestParameters = new HashMap<>(5);
requestParameters.put(SecurityConstants.GRANT_TYPE, SecurityConstants.PASSWORD);
requestParameters.put("phone", request.getParameter("phone"));
requestParameters.put("verifyCode", request.getParameter("verifyCode"));
TokenRequest tokenRequest = new TokenRequest(requestParameters, loginClientId, clientDetails.getScope(), grantTypes);
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
return token;
}
}
@Autowired
private RedisClientDetailsService redisClientDetailsService;
@Autowired
private AuthorizationServerTokenServices tokenServices;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private OAuth2RequestFactory oAuth2RequestFactory;
private OAuth2AccessToken getOauth2AccessToken(String username, String password) {
final HttpServletRequest httpServletRequest = ServletUtils.getRequest();
httpServletRequest.setAttribute(SecurityConstants.LOGIN_TYPE, SecurityConstants.APP_LOGIN);
String loginClientId = "yshop";
ClientDetails clientDetails = null;
try {
clientDetails = redisClientDetailsService.loadClientByClientId(loginClientId);
} catch (Exception e) {
throw new YshopException("获取移动端登录可用的Client失败");
}
if (clientDetails == null) {
throw new YshopException("未找到移动端登录可用的Client");
}
Map<String, String> requestParameters = new HashMap<>(5);
requestParameters.put(SecurityConstants.GRANT_TYPE, SecurityConstants.PASSWORD);
requestParameters.put(USERNAME, username);
requestParameters.put(PASSWORD, password);
String grantTypes = String.join(",", clientDetails.getAuthorizedGrantTypes());
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(requestParameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientDetails.getClientId(),
clientDetails.getScope(),
grantTypes);
OAuth2Request storedOAuth2Request = oAuth2RequestFactory.createOAuth2Request(clientDetails, tokenRequest);
return tokenServices.createAccessToken(new OAuth2Authentication(storedOAuth2Request, userAuth));
}
配置
@Bean
public ResourceOwnerPasswordTokenGranter resourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, OAuth2RequestFactory oAuth2RequestFactory) {
DefaultTokenServices defaultTokenServices = defaultTokenServices();
return new ResourceOwnerPasswordTokenGranter(authenticationManager, defaultTokenServices, redisClientDetailsService, oAuth2RequestFactory);
}
@Bean
public OAuth2RequestFactory oAuth2RequestFactory() {
return new DefaultOAuth2RequestFactory(redisClientDetailsService);
}
}