oauth2.0的使用黑马笔记

一、讲义

讲义

二、代码

地址

三、自定义tokenService

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);
    }

四、redis存储token

  @Autowired
  private RedisConnectionFactory redisConnectionFactory;
  
    @Bean
    public TokenStore tokenStore(){
        return new RedisTokenStore(redisConnectionFactory);
    }
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.tokenStore(tokenStore)
                .tokenServices(new CustomTokenService());
    }

五 增强token


    @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();
    }

七、自定义redis取出来客户端权限资源id等

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

  • 一处是tokenService
  • 一处是资源服务器的clientId等信息处

八、生成token

1、登录成功返回

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;
    }

}


2、自定义登录

    @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);
    }


}


你可能感兴趣的:(笔记)