IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Springboot + Oauth2 单点登录-配置篇 -> 正文阅读

[Java知识库]Springboot + Oauth2 单点登录-配置篇

本文借鉴了网上一些资料,如有侵权,请指出

1. 授权中心配置

所需依赖

<!-- spring security oauth2 开放授权 -->
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.3.5.RELEASE</version>
</dependency>
<!-- security-oauth2自动装配依赖 -->
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.5.6</version>
</dependency>
<!-- security starter依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.5.6</version>
</dependency>
<!-- Token生成与解析-->
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>${jwt.version}</version>
</dependency>
 <dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-jwt</artifactId>
   <version>1.1.1.RELEASE</version>
</dependency>

1.1 认证服务器配置

本次采用Jwt+ Redis方式生成和存储授权信息

关键注解 @EnableAuthorizationServer

/**
 * @date 2020/07/21
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("oauth_client_details")
public class OauthClientDetails implements Serializable {

    @TableId
    private String appId;
    /**
     * 应用名称
     */
    private String appName;
    /**
     * 应用key
     */
    @AttributeUrlParam(name = "clientId")
    private String appKey;
    /**
     * 应用密钥
     */
    private String appSecret;
    /**
     * 资源id集合
     */
    private String resourceIds;
    /**
     * oauth权限范围
     */
    @AttributeUrlParam(name = "scope")
    private String scope;
    /**
     * 支持的授权类型
     */
    @AttributeUrlParam(name = "grantTypes")
    private String authorizedGrantTypes;
    /**
     * 重定向uri
     */
    @AttributeUrlParam(name = "redirectUri")
    private String redirectUri;
    /**
     * security权限值
     */
    private String authorities;
    /**
     * access_token有效时间
     */
    private Integer accessTokenValidity;
    /**
     * refresh_token有效时间
     */
    private Integer refreshTokenValidity;
    /**
     * 客户端的其他信息,必须为json格式
     */
    @AttributeUrlParam(name = "additionalInformation")
    private String additionalInformation;
    /**
     * 是否已存档
     */
    private Integer archived;
    /**
     * 是否是信任的,0:不受信任,1:信任的
     */
    private Integer trusted;
    /**
     * 是否自动跳过approve操作,默认为false,取值范围:true,false,read,write
     */
    @AttributeUrlParam(name = "autoapprove")
    private String autoapprove;
    /**
     * 创建人
     */
    private String createBy;
    /**
     * 创建时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;
    /**
     * 更新人
     */
    private String updateBy;
    /**
     * 更新时间
     */
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

}

1.1.1 重写原生方法支持redis缓存 授权信息

/**
 * 重写原生方法支持redis缓存 授权信息
 * OauthClientDetailsMapper  就是Mapper层
 */
@Slf4j
@Service
public class RedisClientDetailsService extends ServiceImpl<OauthClientDetailsMapper, OauthClientDetails> implements ClientDetailsService {

    @Override
    public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
        // 优先从redis缓存中获取,不存在则从数据库中获取
        OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
        if (StringUtils.isNull(oauthClientDetails)) {
            LambdaQueryWrapper<OauthClientDetails> query = new LambdaQueryWrapper<>();
            query.eq(OauthClientDetails::getAppKey, clientId);
            oauthClientDetails = getOne(query);
            if (StringUtils.isNull(oauthClientDetails)) {
                return null;
            }
            RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.MINUTES);
        }
        return new ClientDetailsAdapter(oauthClientDetails);
    }

}
/**
 * 使用了对象适配器模式
 * OauthClientDetails数据库实体对象 到 Security Oauth2提供的ClientDetails的对象适配器
 *
 * @date 2022/03/23
 */
public class ClientDetailsAdapter implements ClientDetails {

    private final OauthClientDetails clientDetails;

    public ClientDetailsAdapter(OauthClientDetails clientDetails) {
        this.clientDetails = clientDetails;
    }

    @Override
    public String getClientId() {
        return clientDetails.getAppKey();
    }

    @Override
    public Set<String> getResourceIds() {
        return CollectionUtil.getSetBySplit(clientDetails.getResourceIds());
    }

    @Override
    public boolean isSecretRequired() {
        return true;
    }

    @Override
    public String getClientSecret() {
        return clientDetails.getAppSecret();
    }

    /**
     * 客户端是否为特定范围,如果该值返回false,则忽略身份认证的请求范围(scope的值)
     *
     * @return
     */
    @Override
    public boolean isScoped() {
        return true;
    }

    /**
     * 客户端拥有的授权范围
     *
     * @return
     */
    @Override
    public Set<String> getScope() {
        return CollectionUtil.getSetBySplit(clientDetails.getScope());
    }

    /**
     * 客户端拥有的授权方式
     *
     * @return
     */
    @Override
    public Set<String> getAuthorizedGrantTypes() {
        return CollectionUtil.getSetBySplit(clientDetails.getAuthorizedGrantTypes());
    }

    @Override
    public Set<String> getRegisteredRedirectUri() {
        return CollectionUtil.getSetBySplit(clientDetails.getRedirectUri());
    }

    @Override
    public Collection<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> list = new ArrayList<>();
        for (String item : CollectionUtil.getSetBySplit(clientDetails.getAuthorities())) {
            GrantedAuthority authority = new SimpleGrantedAuthority(item);
            list.add(authority);
        }
        return list;
    }

    @Override
    public Integer getAccessTokenValiditySeconds() {
        return clientDetails.getAccessTokenValidity();
    }

    @Override
    public Integer getRefreshTokenValiditySeconds() {
        return clientDetails.getRefreshTokenValidity();
    }

    @Override
    public boolean isAutoApprove(String scope) {
        return false;
    }

    @Override
    public Map<String, Object> getAdditionalInformation() {
        return null;
    }

    /**
     * 第三应用是否可信任
     *
     * @return
     */
    public boolean isTrusted() {
        return clientDetails.getTrusted() == 1;
    }

    public OauthClientDetails getClientDetails() {
        return clientDetails;
    }
}

1.1.2 认证服务器 配置

/**
* 认证服务器
*
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;
    
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private RedisClientDetailsService oauthClientDetailsService;
    
    /**
    * 定义授权和令牌端点以及令牌服务
    */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
            // 请求方式 post
            .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST)
            // 指定token存储位置
            .tokenStore(tokenStore())
            .tokenServices(tokenService())
            // 用户账号密码认证
            .userDetailsService(userDetailsService)
            // 用户授权确认处理器
            .userApprovalHandler(userApprovalHandler())
            // 如果用密码模式验证,则需要配置 指定认证管理器
            .authenticationManager(authenticationManager)
            // 自定义异常处理
            .exceptionTranslator(new CustomWebResponseExceptionTranslator());
    }
    
    /**
    * 在JWT编码的令牌值和OAuth身份验证信息(双向)之间转换的助手。 
    * 授予令牌时,还充当TokenEnhancer
    * JWT令牌转换器可以配置密钥对
    * 密钥对需要密钥文件,即( .jks 或者 .keystore) 文件,
    * 件里保存了密钥信息,密钥包含了私钥和公钥
    * 
    */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new MyJwt();
        converter.setSigningKey(SecurityConstants.JWT_ACCESS_SIGNING_KEY);
        return converter;
    }
    
    
   
    
    /**
    * 令牌(token)管理服务
    */
    @Bean
    public AuthorizationServerTokenServices tokenService(){
        DefaultTokenServices services = new DefaultTokenServices();
        // 客户端详情服务
        // 因为是向客户端颁发令牌,所以需要知道是哪一个客户端
        services.setClientDetailsService(oauthClientDetailsService);
        // 令牌存储策略
        services.setTokenStore(tokenStore());
        //令牌增强
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new LinkedList<>();
        delegates.add(jwtAccessTokenConverter());
        //        delegates.add(tokenEnhancer());
        tokenEnhancerChain.setTokenEnhancers(delegates);
        services.setTokenEnhancer(tokenEnhancerChain);
        
        // 支持刷新令牌
        services.setSupportRefreshToken(true);
        // 是否重复使用 refresh_token
        services.setReuseRefreshToken(true);
        return services;
    }
    
    /**
    * 配置令牌端点(Token Endpoint)的安全约束
    */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
        /**
        * 默认访问安全规则是denyAll(),即默认情况下是关闭的,请求调不通
        * 在资源服务器中,可能会调用oauth/token_key 来请求获取公钥,如果这里调不通,
        * 资源服务就没办法获取公钥,就没办法对jwt令牌的签名算法进行解密
        */
        // 允许 /oauth/token的端点表单认证
        oauthServer.allowFormAuthenticationForClients()
            .tokenKeyAccess("isAuthenticated()")
            // 允许 /oauth/token_check端点的访
            .checkTokenAccess("permitAll()")
            .addTokenEndpointAuthenticationFilter(new CorsFilter(source));
    }
    
    /**
    * 客户端配置
    */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(oauthClientDetailsService);
    }
    
    /**
    * 基于 Redis 实现,令牌保存到缓存
    */
    @Bean
    public TokenStore tokenStore() {
        RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
        // 前缀
        tokenStore.setPrefix(Oauth2Constants.OAUTH_ACCESS);
        // 序列化策略,使用fastjson
        tokenStore.setSerializationStrategy(new FastJsonRedisTokenStoreSerializationStrategy());
        return tokenStore;
    }
    
    @Bean
    public OAuth2RequestFactory oAuth2RequestFactory() {
        return new DefaultOAuth2RequestFactory(oauthClientDetailsService);
    }
    
    @Bean
    public UserApprovalHandler userApprovalHandler() {
        OauthUserApprovalHandler userApprovalHandler = new OauthUserApprovalHandler();
        userApprovalHandler.setTokenStore(tokenStore());
        userApprovalHandler.setClientDetailsService(oauthClientDetailsService);
        userApprovalHandler.setRequestFactory(oAuth2RequestFactory());
        return userApprovalHandler;
    }
    
}

1.1.3 增强令牌实现

/**
 * 定制 jwt 自定义token信息
 * @date 2022/4/21 0:41
 */
public class MyJwt extends JwtAccessTokenConverter {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        //设置登录人缓存信息
        LoginUser loginUser = SecurityUtils.getLoginUser();
        //获取客户端clientId
        LinkedHashMap<String, Object> clientDetail = (LinkedHashMap<String, Object>) authentication.getUserAuthentication().getDetails();
        //查询缓存中应用配置信息
        OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientDetail.get(Oauth2Constants.CLIENT_ID));

        Map<String, Object> additionalInfo = new LinkedHashMap<>();
        Map<String, Object> info = new LinkedHashMap<>();
        info.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.LICENSE);
        info.put(Oauth2Constants.TOKEN_KRY, loginUser.getTokenKey());
        additionalInfo.put("info", info);
        //设置token的过期时间 分钟数
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.MINUTE,  oauthClientDetails.getAccessTokenValidity());
        loginUser.setExpireTime(nowTime.getTime());
        //缓存登录人信息
        RedisUtils.setCacheObject(RedisUtils.getTokenKey(loginUser.getTokenKey()), loginUser, oauthClientDetails.getAccessTokenValidity(), TimeUnit.MINUTES);
        //设置Oauth2
        ((DefaultOAuth2AccessToken) accessToken).setExpiration(nowTime.getTime());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
        return super.enhance(accessToken, authentication);
    }

}

1.2 资源服务器配置

/**
 * Oauth授权服务器配置,Oauth资源服务器配置
 */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "USER-RESOURCE";

    @Autowired
    private OauthTokenExtractor oauthTokenExtractor;

    @Autowired
    private CustomAccessDeniedHandler accessDeniedHandler;

    @Autowired
    private CustomAuthExceptionEntryPoint exceptionEntryPoint;

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 所有请求必须认证通过
        http.authorizeRequests()
                // 过滤放行
                .antMatchers("/v2/api-docs").permitAll()
                .antMatchers("/swagger-resources/configuration/ui").permitAll()
                .antMatchers("/swagger-resources").permitAll()
                .antMatchers("/swagger-resources/configuration/security").permitAll()
                .antMatchers("/swagger-ui.html").permitAll()
                .anyRequest().authenticated();
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID).stateless(false);
        // token提取器
        resources.tokenExtractor(oauthTokenExtractor)
                // token异常处理器
                .authenticationEntryPoint(exceptionEntryPoint)
                // 无权限异常处理器
                .accessDeniedHandler(accessDeniedHandler);
    }

}

1.2.1 oauth资源服务器的token提取器

/**
 * oauth资源服务器的token提取器,用于获取请求中的access_token
 * 如果没有什么特殊要求可以直接使用 {@link org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor}
 */
@Slf4j
@Component
public class OauthTokenExtractor implements TokenExtractor {
    @Override
    public Authentication extract(HttpServletRequest request) {
        String tokenValue = this.extractToken(request);
        if (tokenValue != null) {
            return new PreAuthenticatedAuthenticationToken(tokenValue, "");
        } else {
            return null;
        }
    }

    public String extractToken(HttpServletRequest request) {
        String token = this.extractHeaderToken(request);
        if (token == null) {
            log.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
            if (token == null) {
                log.debug("Token not found in request parameters.  Trying request cookies.");

                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
                token = extractCookieToken(request);
                if (token == null) {
                    log.debug("Token not found in cookies.  Not an OAuth2 request.");
                } else {
                    request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
                }
            }
        }
        return token;
    }

    private String extractHeaderToken(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaders(Oauth2Constants.TOKEN_HEADER);
        while (headers.hasMoreElements()) {
            //通常只有一个(大多数服务器强制执行)
            String value = headers.nextElement();
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // 将此添加到此处,以获取稍后的身份验证详细信息。最好更改此方法的签名。
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }
        return null;
    }

    private String extractCookieToken(HttpServletRequest request) {

        String cookieToken = null;
        //根据请求数据,找到cookie数组
        Cookie[] cookies = request.getCookies();

        if (null != cookies && cookies.length > 0) {
            for (Cookie cookie : cookies) {
                if (null != cookie.getName() && cookie.getName().trim().equalsIgnoreCase(OAuth2AccessToken.ACCESS_TOKEN)) {
                    cookieToken = cookie.getValue().trim();
                    break;
                }
            }
        }
        return cookieToken;
    }
}

1.2.2 自定义访问无权限资源时的异常

/**
 * 自定义访问无权限资源时的异常
 *
 */
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        log.info("权限不足,请联系管理员 {}", request.getRequestURI());

        int code = HttpStatus.FORBIDDEN;
        String msg = accessDeniedException.getMessage();
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

1.2.3 自定义未登录处理

/**
 * 自定义未登录处理
 * AuthenticationEntryPoint是Spring Security Web一个概念模型接口,
 * 顾名思义,他所建模的概念是:“认证入口点”。
 * 它在用户请求处理过程中遇到认证异常时,
 * 被ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程。
 */
@Slf4j
@Component
public class CustomAuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    private RedisClientDetailsService clientDetailsService;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        log.info("令牌不合法,禁止访问 {}", request.getRequestURI());
        //获得请求参数
        String clientId = request.getParameter(Oauth2Constants.CLIENT_ID);
        if(StringUtils.isBlank(clientId)){
            clientId = request.getParameter(Oauth2Constants.MY_CLIENT_ID);
        }
        // 查询客户端id  优先从redis缓存中获取,不存在则从数据库中获取
        // 优先从redis缓存中获取,不存在则从数据库中获取
        OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
        if (StringUtils.isNull(oauthClientDetails)) {
            LambdaQueryWrapper<OauthClientDetails> lqw = Wrappers.lambdaQuery();
            oauthClientDetails = clientDetailsService.getOne(lqw.eq(OauthClientDetails::getAppKey, clientId));
            if (StringUtils.isNull(oauthClientDetails)) {
                ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, "未识别应用",null)));
                return;
            }else {
                RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.SECONDS);
            }
        }
        String param = StringUtils.formatUrlParamsWithAnnocation(oauthClientDetails);

        String msg = authException.getMessage();
        //重定向到授权登录页
        String authUrl = "http://localhost:8200/auth/login?" + param;
        ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, msg, authUrl)));
    }

}

1.2.4 自定义实体类属性转换为url参数

/** 
* 将实体类clazz的属性转换为url参数
* @param clazz 参数实体类
* 
*/
public static String formatUrlParamsWithAnnocation(Object clazz) {
    // 遍历属性类、属性值
    Field[] fields = clazz.getClass().getDeclaredFields();

    StringBuilder requestURL = new StringBuilder();
    try {
        boolean flag = true;
        String property, value;
        for (Field field : fields) {
            //获得注解
            // 容许访问私有变量
            field.setAccessible(true);
            boolean annotationPresent = field.isAnnotationPresent(AttributeUrlParam.class);
            if(annotationPresent){
                //获得注解
                AttributeUrlParam urlParam = field.getAnnotation(AttributeUrlParam.class);
                // 属性名
                property = urlParam.name();
                // 属性值
                value = field.get(clazz).toString();

                String params = property + "=" + value;
                if (flag) {
                    requestURL.append(params);
                    flag = false;
                } else {
                    requestURL.append("&").append(params);
                }
            }
        }
    } catch (Exception ignored) {

    }
    return requestURL.toString();
}

自定义注解

import java.lang.annotation.*;

/**
 * 自定义注解拼接Url参数
 * @author smartby
 */
@Inherited
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AttributeUrlParam {

    /**
     * Url参数名
     */
    String name() default "";
}
/**
 * 开启Spring Security方法级安全
 *
 * prePostEnabled: 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用
 * securedEnabled: 确定安全注解 [@Secured] 是否启用
 * jsr250Enabled: 确定 JSR-250注解 [@RolesAllowed..]是否启用
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private CustomAuthExceptionEntryPoint unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
            // CSRF禁用,因为不使用session
            .csrf().disable()
            // 认证失败处理类
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .antMatchers("/login", "/register", "/captchaImage").anonymous()
            .antMatchers(
                HttpMethod.GET,
                "/",
                "/*.html",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
            ).permitAll()
            .antMatchers("/profile/**").anonymous()
            .antMatchers("/auth/**").anonymous()
            .antMatchers("/doc.html").anonymous()
            .antMatchers("/swagger-resources/**").anonymous()
            .antMatchers("/webjars/**").anonymous()
            .antMatchers("/*/api-docs").anonymous()
            .antMatchers("/druid/**").anonymous()
            // Spring Boot Actuator 的安全配置
            .antMatchers("/actuator").anonymous()
            .antMatchers("/actuator/**").anonymous()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated()
            .and()
            .headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    /*** 设置不拦截规则 */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .antMatchers("/login", "/register", "/captchaImage")
            .antMatchers(
                HttpMethod.GET,
                "/",
                "/*.html",
                "/**/*.html",
                "/**/*.css",
                "/**/*.js"
            )
            .antMatchers("/profile/**")
            .antMatchers("/auth/**")
            .antMatchers("/doc.html")
            .antMatchers("/swagger-resources/**")
            .antMatchers("/webjars/**")
            .antMatchers("/*/api-docs")
            .antMatchers("/druid/**")
            // Spring Boot Actuator 的安全配置
            .antMatchers("/actuator")
            .antMatchers("/actuator/**")
            .antMatchers( "/v2/**", "/swagger-resources/**");
    }

}

1.3 其他一些工具类

1.3.1 自定义默认的刷新token序列化工具类

/**
 * 自定义默认的刷新token序列化工具类
 *
 * @date 2022/03/01
 */
public class DefaultOauth2RefreshTokenSerializer implements ObjectDeserializer {

    @Override
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        if (type == DefaultOAuth2RefreshToken.class) {
            JSONObject jsonObject = parser.parseObject();
            String tokenId = jsonObject.getString("value");
            DefaultOAuth2RefreshToken refreshToken = new DefaultOAuth2RefreshToken(tokenId);
            return (T) refreshToken;
        }
        return null;
    }

    @Override
    public int getFastMatchToken() {
        return 0;
    }

}

1.3.2 redis存储json格式序列化反序列化工具类

/**
 * fastjson redis存储json格式序列化反序列化工具类
 */
public class FastJsonRedisTokenStoreSerializationStrategy implements RedisTokenStoreSerializationStrategy {

    private final static ParserConfig DEFAULT_REDIS_CONFIG = new ParserConfig();

    static {
        // 自定义oauth2序列化,
        DEFAULT_REDIS_CONFIG.setAutoTypeSupport(true);
        自定义DefaultOauth2RefreshTokenSerializer反序列化
        DEFAULT_REDIS_CONFIG.putDeserializer(DefaultOAuth2RefreshToken.class, new DefaultOauth2RefreshTokenSerializer());
        自定义OAuth2Authentication反序列化
        DEFAULT_REDIS_CONFIG.putDeserializer(OAuth2Authentication.class, new OAuth2AuthenticationSerializer());
        //添加autotype白名单
        DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.provider.");
        DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.provider.client");
        TypeUtils.addMapping("org.springframework.security.oauth2.provider.OAuth2Authentication", OAuth2Authentication.class);
        TypeUtils.addMapping("org.springframework.security.oauth2.provider.client.BaseClientDetails", BaseClientDetails.class);

        DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.oauth2.common.");
        TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultOAuth2AccessToken", DefaultOAuth2AccessToken.class);
        TypeUtils.addMapping("org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken", DefaultExpiringOAuth2RefreshToken.class);

        DEFAULT_REDIS_CONFIG.addAccept("com.pthin.oauth.server.adapter");
        TypeUtils.addMapping("com.pthin.oauth.server.adapter.UserDetailsAdapter", LoginUser.class);

        DEFAULT_REDIS_CONFIG.addAccept("org.springframework.security.web.authentication.preauth");
        TypeUtils.addMapping("org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken", PreAuthenticatedAuthenticationToken.class);
    }

    @Override
    public <T> T deserialize(byte[] bytes, Class<T> clazz) {
        Assert.notNull(clazz, "clazz can't be null");
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        try {
            return JSON.parseObject(new String(bytes, IOUtils.UTF8), clazz, DEFAULT_REDIS_CONFIG);
        } catch (Exception e) {
            throw new SerializationException("Could not serialize: " + e.getMessage(), e);
        }
    }

    @Override
    public String deserializeString(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return null;
        }
        return new String(bytes, IOUtils.UTF8);
    }

    @Override
    public byte[] serialize(Object object) {
        if (object == null) {
            return new byte[0];
        }
        try {
            return JSON.toJSONBytes(object, SerializerFeature.WriteClassName, SerializerFeature.DisableCircularReferenceDetect);
        } catch (Exception e) {
            throw new SerializationException("Could not serialize: " + e.getMessage(), e);
        }
    }

    @Override
    public byte[] serialize(String data) {
        if (StringUtils.isEmpty(data)) {
            return new byte[0];
        }
        return data.getBytes(IOUtils.UTF8);
    }

}

序列化

/**
 * @date 2020/07/29
 */
public class OAuth2AuthenticationSerializer implements ObjectDeserializer {
    @Override
    public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
        if (type == OAuth2Authentication.class) {
            try {
                Object o = parse(parser);
                if (o == null) {
                    return null;
                } else if (o instanceof OAuth2Authentication) {
                    return (T) o;
                }

                JSONObject jsonObject = (JSONObject) o;
                OAuth2Request request = parseOAuth2Request(jsonObject);

                //判断json节点userAuthentication的类型,根据类型动态取值
                //UsernamePasswordAuthenticationToken 密码模式/授权码模式下,存储类型为UsernamePasswordAuthenticationToken
                //PreAuthenticatedAuthenticationToken 刷新token模式下,存储类型为PreAuthenticatedAuthenticationToken
                Object autoType = jsonObject.get("userAuthentication");
                return (T) new OAuth2Authentication(request, jsonObject.getObject("userAuthentication", (Type) autoType.getClass()));
            } catch (Exception e) {
                e.printStackTrace();
            }
            return null;
        }
        return null;
    }

    @Override
    public int getFastMatchToken() {
        return 0;
    }

    private OAuth2Request parseOAuth2Request(JSONObject jsonObject) {
        JSONObject json = jsonObject.getObject("oAuth2Request", JSONObject.class);
        Map<String, String> requestParameters = json.getObject("requestParameters", Map.class);
        String clientId = json.getString("clientId");
        String grantType = json.getString("grantType");
        String redirectUri = json.getString("redirectUri");
        Boolean approved = json.getBoolean("approved");
        Set<String> responseTypes = json.getObject("responseTypes", new TypeReference<HashSet<String>>() {
        });
        Set<String> scope = json.getObject("scope", new TypeReference<HashSet<String>>() {
        });
        Set<String> authorities = json.getObject("authorities", new TypeReference<HashSet<String>>() {
        });
        Set<GrantedAuthority> grantedAuthorities = new HashSet<>(0);
        if (authorities != null && !authorities.isEmpty()) {
            authorities.forEach(s -> grantedAuthorities.add(new SimpleGrantedAuthority(s)));
        }
        Set<String> resourceIds = json.getObject("resourceIds", new TypeReference<HashSet<String>>() {
        });
        Map<String, Serializable> extensions = json.getObject("extensions", new TypeReference<HashMap<String, Serializable>>() {
        });

        OAuth2Request request = new OAuth2Request(requestParameters, clientId, grantedAuthorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);
        TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scope, grantType);
        request.refresh(tokenRequest);
        return request;
    }

    private Object parse(DefaultJSONParser parse) {
        JSONObject object = new JSONObject(parse.lexer.isEnabled(Feature.OrderedField));
        Object parsedObject = parse.parseObject(object);
        if (parsedObject instanceof JSONObject) {
            return (JSONObject) parsedObject;
        } else if (parsedObject instanceof OAuth2Authentication) {
            return parsedObject;
        } else {
            return parsedObject == null ? null : new JSONObject((Map) parsedObject);
        }
    }
}

1.4 自定义处理器

1.4.1 认证成功处理


/**
 * 认证成功处理
 *
 */
@Slf4j
@Component
public class AuthenticationSuccessEventHandler implements ApplicationListener<AuthenticationSuccessEvent> {

    @Autowired
    private LogininforService asyncService;

    @Override
    public void onApplicationEvent(AuthenticationSuccessEvent event) {
        Authentication authentication = (Authentication) event.getSource();
        if (StringUtils.isNotNull(authentication.getPrincipal()) && authentication.getPrincipal() instanceof LoginUser) {
            HttpServletRequest request = ServletUtils.getRequest();

            String url = request.getRequestURI();

            LoginUser user = (LoginUser) authentication.getPrincipal();

            String username = user.getUsername();

            log.info("用户:{} 授权成功,url:{}", username, url);

            // 登录日志记录
            if (StringUtils.containsAny(url, SecurityConstants.AUTH_TOKEN, SecurityConstants.TOKEN_LOGOUT)) {
                String status = StringUtils.equals(SecurityConstants.AUTH_TOKEN, url) ? Constants.LOGIN_SUCCESS : Constants.LOGOUT;
                String message = StringUtils.equals(SecurityConstants.AUTH_TOKEN, url) ? "登录成功" : "退出成功";
                asyncService.recordLogininfor(username, status, message,request);
            }
        }
    }
}

1.4.2 认证失败处理

/**
 * 认证失败处理
 *
 */
@Slf4j
@Component
public class AuthenticationFailureEvenHandler implements ApplicationListener<AbstractAuthenticationFailureEvent> {

    @Autowired
    private LogininforService asyncService;

    @Override
    public void onApplicationEvent(AbstractAuthenticationFailureEvent event) {
        Authentication authentication = (Authentication) event.getSource();
        if (StringUtils.isNotNull(authentication.getPrincipal())) {
            HttpServletRequest request = ServletUtils.getRequest();

            String url = request.getRequestURI();

            String username = (String) authentication.getPrincipal();

            log.info("用户:{} 认证失败,无法访问系统资源url:{}", username, url);

            // 登录失败日志记录
            if (StringUtils.equals(url, SecurityConstants.AUTH_TOKEN) && StringUtils.isNotNull(authentication.getDetails())) {
                // 记录用户退出日志
                asyncService.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match"),request);
            }
            int code = HttpStatus.HTTP_UNAUTHORIZED;
            String msg = StringUtils.format("请求访问:{},认证失败,无法访问系统资源", request.getRequestURI());
            ServletUtils.renderString(ServletUtils.getResponse(), JsonUtils.toJsonString(AjaxResult.error(code, msg)));
        }

    }
}

1.4.3 自定义访问无权限资源时的异常

/**
 * 自定义访问无权限资源时的异常
 *
 */
@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) {
        log.info("权限不足,请联系管理员 {}", request.getRequestURI());

        int code = HttpStatus.FORBIDDEN;
        String msg = accessDeniedException.getMessage();
        ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
    }
}

1.4.4 自定义未登录处理

/**
 * 自定义未登录处理
 * AuthenticationEntryPoint是Spring Security Web一个概念模型接口,
 * 顾名思义,他所建模的概念是:“认证入口点”。
 * 它在用户请求处理过程中遇到认证异常时,
 * 被ExceptionTranslationFilter用于开启特定认证方案(authentication schema)的认证流程。
 *
 */
@Slf4j
@Component
public class CustomAuthExceptionEntryPoint implements AuthenticationEntryPoint {

    @Autowired
    private RedisClientDetailsService clientDetailsService;

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        log.info("令牌不合法,禁止访问 {}", request.getRequestURI());
        //获得请求参数
        String clientId = request.getParameter(Oauth2Constants.CLIENT_ID);
        if(StringUtils.isBlank(clientId)){
            clientId = request.getParameter(Oauth2Constants.MY_CLIENT_ID);
        }
        // 查询客户端id  优先从redis缓存中获取,不存在则从数据库中获取
        // 优先从redis缓存中获取,不存在则从数据库中获取
        OauthClientDetails oauthClientDetails = RedisUtils.getCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId);
        if (StringUtils.isNull(oauthClientDetails)) {
            LambdaQueryWrapper<OauthClientDetails> lqw = Wrappers.lambdaQuery();
            oauthClientDetails = clientDetailsService.getOne(lqw.eq(OauthClientDetails::getAppKey, clientId));
            if (StringUtils.isNull(oauthClientDetails)) {
                ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, "未识别应用",null)));
                return;
            }else {
                RedisUtils.setCacheObject(Oauth2Constants.CLIENT_DETAILS_KEY + clientId, oauthClientDetails, oauthClientDetails.getAccessTokenValidity(), TimeUnit.SECONDS);
            }
        }
        String param = StringUtils.formatUrlParamsWithAnnocation(oauthClientDetails);

        String msg = authException.getMessage();
        String authUrl = "http://localhost:8200/auth/login?" + param;
        ServletUtils.renderString(response, JSONObject.toJSONString(AjaxResult.error(HttpStatus.UNAUTHORIZED, msg, authUrl)));
    }

}

1.4.5 自定义退出处理类 返回成功

/**
 * 自定义退出处理类 返回成功
 *
 */
@Configuration
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {


    @Autowired
    private TokenService tokenService;

    @Autowired
    private LogininforService asyncService;

    /**
     * 退出处理
     */
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser)) {
            String userName = loginUser.getUsername();
            // 删除用户缓存记录
            tokenService.delLoginUser(loginUser.getTokenKey());
            // 记录用户退出日志
            asyncService.recordLogininfor(userName, Constants.LOGOUT, "退出成功", request);
        }
        ServletUtils.renderString(response, JsonUtils.toJsonString(AjaxResult.error(HttpStatus.HTTP_OK, "退出成功")));
    }

}

1.4.6 增加受信任的第三方处理

/**
 * 重写当对于需要询问是否授权第三方应用时的方法,增加受信任的第三方处理
 */
@Slf4j
public class OauthUserApprovalHandler extends TokenStoreUserApprovalHandler {
    /**
     * 是否已批准授权(第三方应用)
     */
    @Override
    public boolean isApproved(AuthorizationRequest authorizationRequest, Authentication userAuthentication) {
        if (super.isApproved(authorizationRequest, userAuthentication)) {
            return true;
        }
        if (!userAuthentication.isAuthenticated()) {
            return false;
        }
        RedisClientDetailsService oauthClientDetailsService = SpringUtils.getBean(RedisClientDetailsService.class);

        ClientDetailsAdapter clientDetails = (ClientDetailsAdapter) oauthClientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
        return clientDetails != null && clientDetails.isTrusted();
    }
}

1.5 自定义异常处理

/**
 * OAuth2 自定义异常处理

 */
@Slf4j
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {
    @Override
    public ResponseEntity<OAuth2Exception> translate(Exception e) {

        OAuth2Exception oAuth2Exception = (OAuth2Exception) e;
        log.error("CustomWebResponseExceptionTranslator.status:{},oAuth2ErrorCode:{},message:{}",
                oAuth2Exception.getHttpErrorCode(), oAuth2Exception.getOAuth2ErrorCode(), oAuth2Exception.getMessage());
        return ResponseEntity.status(HttpServletResponse.SC_UNAUTHORIZED).body(oAuth2Exception);
    }

}

1.6 常量

/**
 * 缓存的key 常量
 *
 */
public class Oauth2Constants {

    /**
     * 构建风格 1 elementui  2 antDesign
     */
    public static final int IS_ELEMENTUI_MENU = 1;

    //===========================================================
    /**
     * oauth 缓存前缀
     */
    public static final String OAUTH_ACCESS = "oauth:access:";

    /**
     * oauth 客户端信息
     */
    public static final String CLIENT_DETAILS_KEY = "oauth:client:details:";

    /**
     * 用户id
     */
    public static final String CLIENT_USER_ID = "oauth:client:userId:";

    /**
     * token 请求头
     */
    public static final String TOKEN_HEADER = "Authorization";

    /**
     * 客户端ID
     */
    public static final String CLIENT_ID = "client_id";

    /**
     * 客户端ID
     */
    public static final String MY_CLIENT_ID = "clientId";

    /**
     * redis 缓存的tokenkey
     */
    public static final String TOKEN_KRY = "tokenKey";
    /**
     * 授权地址
     */
    public static final String OAUTH2_ACCESS_TOKEN = "http://127.0.0.1:9500/oauth/token?client_id={clientId}" +
            "&client_secret={clientSecret}&grant_type={grantType}&username={username}&password={password}";

    /**
     * 校验地址
     */
    public static final String OAUTH2_CHECK_TOKEN = "http://127.0.0.1:9500/oauth/check_token?token={token}";

    /**
     * 刷新token地址
     */
    public static final String OAUTH2_REFRESH_TOKEN = "http://127.0.0.1:9500/oauth/token?grant_type=refresh_token" +
        "&refresh_token={token}&client_id={clientId}&client_secret={clientSecret}";

    //======================================授权模式====================================
    /**
     * password 密码模式
     */
    public static final String GRANT_TYPE_PASSWORD = "password";

    /**
     * authorization_code 授权码模式
     */
    public static final String GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code";

    /**
     * client_credentials 客户端模式
     */
    public static final String GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";

}

1.7 管理令牌

1.7 管理令牌方式

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。

自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类,里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore :如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都实现了TokenStore接口:

InMemoryTokenStore :这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore :这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时,可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到classpath当中。

JwtTokenStore :这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

RedisTokenStore:

在使用Redis存储token,spring security oauth2 会生成以下几个key, 直接放出RedisTokenStore的源码吧

private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";

看一下RedisTokenStore的源码

//spring secuity oauth2提供的一个序列化工具
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
//存储OAuth2AccessToken
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    byte[] serializedAccessToken = serialize(token);
    ......
    }
//把存储对象序列化
private byte[] serialize(Object object) {
    return serializationStrategy.serialize(object);
}

通过源码,我们发现spring security oauth2存储的是序列化后的对象,而不是json。(注意:应该是为提高存储效率,而不是加密操作)

那我们就以access:[AccessToken]这个key为例,在自己项目里写一个controller或者junit单元测试

@ApiOperation(value = "获取access存储内容", httpMethod = "GET")
@GetMapping("/deserialize/access")
public R<Object> deserializeAccessToken(@NotBlank(message = "accessToken不能为空"),
                                        @RequestParam("accessToken") String accessToken) {
    RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
    byte[] serializedKey = serializationStrategy.serialize(Prefix.REDIS_SHIELD_OAUTH2 + "access:" + accessToken);
    RedisConnection conn = connectionFactory.getConnection();
    byte[] bytes;
    try {
        bytes = conn.get(serializedKey);
    } finally {
        conn.close();
    }
    OAuth2AccessToken content = serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
    return RUtil.success(content);
}

结果如下

{
  "access_token": "x8U6xmAK0MeFDEJ0",
  "token_type": "bearer",
  "refresh_token": "0qLDRZE70MeFDEI!",
  "expires_in": 29658,
  "scope": "server"
  }

1.7.1 access:[AccessToken](对应对象:OAuth2AccessToken)

要如何获取这个key? 如果你已有spring security oauth2的项目(如果没有,可以github或spring官网找一个sample项目或自己搭建一个),可以类似这样发送一个请求:

http://localhost:6799/oauth/token?grant_type=password&username=wuji&password=12345678&client_id=app&client_secret=app

spring security oauth2获取AccessToken如果是密码授权方式(grant_type=password)除了携带用户名以及密码还要携带client_id和client_secret,当然,你也可以Http Basic方式将client_id和client_secret到请求头,如下

YXBwOmFwcA==是“client_id:client_secret” base64编码后的结果。
我们试着发送一个请求,返回结果如下:

{
  //token,拿着这个token我们就可以资源(接口)了
  "access_token": "x8U6xmAK0MeFDEJ0",
  //token类型是一个票据类型,还有授权码类型等等
  "token_type": "bearer",
  //用与刷新access_token(资源访问token)的token
  "refresh_token": "0qLDRZE70MeFDEI!",
  //access_token剩余存活时间(单位是秒)
  "expires_in": 29658,
  //拿这个token可以访问那些范围内的资源
  "scope": "server"
}

1.7.2 auth_to_access:[这里不再是AccessToken]

阅读源码,发现

public String extractKey(OAuth2Authentication authentication) {
    //省略一些代码
    values.put(USERNAME, authentication.getName());
    values.put(CLIENT_ID, authorizationRequest.getClientId());
    values.put(SCOPE, OAuth2Utils.formatParameterList(new TreeSet<String>(authorizationRequest.getScope())));
    return generateKey(values);
}

auth_to_access:key, key是将username、client_id、scope三个值加密后的值,我们再看auth_to_access:存储的内容如下

{
  "access_token": "x8U6xmAK0MeFDEJ0",
  "token_type": "bearer",
  "refresh_token": "0qLDRZE70MeFDEI!",
  "expires_in": 24126,
  "scope": "server"
}

跟access:的内容一模一样,那我们就可以知道,代码内部实现可以通过username、client_id、scope 3个字段获取AccessToken。

1.7.3 auth:[accessToken] (对应对象:OAuth2Authentication)

auth:存的内容如下:

{
    "authorities": [{
            "authority": "ROLE_USER"
        },
        {
            "authority": "USER_RETRIEVE"
        }
    ],
    "details": null,
    "authenticated": true,
    "userAuthentication": {
        "authorities": [{
                "authority": "ROLE_USER"
            },
            {
                "authority": "USER_RETRIEVE"
            }
        ],
        "details": {
            "client_secret": "app",
            "grant_type": "password",
            "client_id": "app",
            "username": "wuji"
        },
        "authenticated": true,
        "principal": {
            "password": null,
            "username": "wuji",
            "authorities": [{
                    "authority": "ROLE_USER"
                },
                {
                    "authority": "USER_RETRIEVE"
                }
            ],
            "accountNonExpired": true,
            "accountNonLocked": true,
            "credentialsNonExpired": true,
            "enabled": true
        },
        "credentials": null,
        "name": "wuji"
    },
    "credentials": "",
    "principal": {
        "password": null,
        "username": "wuji",
        "authorities": [{
                "authority": "ROLE_USER"
            },
            {
                "authority": "USER_RETRIEVE"
            }
        ],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "oauth2Request": {
        "clientId": "app",
        "scope": ["server"],
        "requestParameters": {
            "grant_type": "password",
            "client_id": "app",
            "username": "wuji"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": null,
        "responseTypes": [],
        "extensions": {},
        "grantType": "password",
        "refreshTokenRequest": null
    },
    "clientOnly": false,
    "name": "wuji"
}

主要包含当前登录用户的信息,以及用户附带的角色和和权限信息、生成Token时的授权方式等信息。

1.7.4 access_to_refresh:[accessToken]

access_to_refresh:存储的内容很简单,在通过password等授权方式获取token时的refreshToken

//refresh_token
grz0Xlzi0MeQwkx9

1.7.5 refresh_to_access:[refreshToken]

拿refreshToken去刷新accessToken时,会将新生成的accessToken放到refresh_to_access:

//access_token
TesxUOBt0MeRDDxA

1.7.6 refresh:[refreshToken]

存储内容如下:

{
 //refresh_toekn
  "value": "grz0Xlzi0MeQwkx9",
  //过期时间戳(mills)
  "expiration": 1557821765322
}

拿refreshToken去刷新accessToken时, 会先拿到这个KEY的信息,判断请求方的refresh token是否有效,无效的不能刷新access token。

1.7.7 refresh_auth:[refreshToken] (对应对象:OAuth2Authentication)

refresh_auth:存的内容与auth:类似,如下:

{
  "authorities": [
    {
      "authority": "ROLE_USER"
    },
    {
      "authority": "USER_RETRIEVE"
    }
  ],
  "details": null,
  "authenticated": true,
  "userAuthentication": {
    "authorities": [
      {
        "authority": "ROLE_USER"
      },
      {
        "authority": "USER_RETRIEVE"
      }
    ],
    "details": {
      "client_secret": "app",
      "grant_type": "password",
      "client_id": "app",
      "username": "wuji"
    },
    "authenticated": true,
    "principal": {
      "password": null,
      "username": "wuji",
      "authorities": [
        {
          "authority": "ROLE_USER"
        },
        {
          "authority": "USER_RETRIEVE"
        }
      ],
      "accountNonExpired": true,
      "accountNonLocked": true,
      "credentialsNonExpired": true,
      "enabled": true
    },
    "credentials": null,
    "name": "wuji"
  },
  "credentials": "",
  "principal": {
    "password": null,
    "username": "wuji",
    "authorities": [
      {
        "authority": "ROLE_USER"
      },
      {
        "authority": "USER_RETRIEVE"
      }
    ],
    "accountNonExpired": true,
    "accountNonLocked": true,
    "credentialsNonExpired": true,
    "enabled": true
  },
  "oauth2Request": {
    "clientId": "app",
    "scope": ["server"],
    "requestParameters": {
      "grant_type": "password",
      "client_id": "app",
      "username": "wuji"
    },
    "resourceIds": [],
    "authorities": [],
    "approved": true,
    "refresh": false,
    "redirectUri": null,
    "responseTypes": [],
    "extensions": {},
    "grantType": "password",
    "refreshTokenRequest": null
  },
  "clientOnly": false,
  "name": "wuji"
}

1.7.8 client_id_to_access:[client_id]

[{
    "access_token": "TesxUOBt0MeRDDxA",
    "token_type": "bearer",
    "refresh_token": "grz0Xlzi0MeQwkx9",
    "expires_in": 41714,
    "scope": "server"
}]

顾名思义,这个key将对应client_id的AccessToken对象存储了起来,因为不同的username、scope使用同一个client_id去请求获取token,所以这是一个list。

1.7.9 uname_to_access:[client_id:username]

[{
    "access_token": "TesxUOBt0MeRDDxA",
    "token_type": "bearer",
    "refresh_token": "grz0Xlzi0MeQwkx9",
    "expires_in": 41714,
    "scope": "server"
}]

这里也是一个list。

2. 客户端配置

2.1 application.yml 配置

security:
  oauth2:
    client:
      # 客户端标识
      client-id: test
      # 客户端秘钥
      client-secret: 123456    
      #地址为授权中心IP和端口
      access-token-uri: http://127.0.0.1:9500/oauth/token
      user-authorization-uri: http://127.0.0.1:9500/oauth/authorize
    resource:
         user-info-uri: http://127.0.0.1:9500/user
         token-info-uri: http://127.0.0.1:9500/oauth/check_token
      jwt:
        key-uri: http://127.0.0.1:9500/oauth/token_key

2.2 pom依赖

<!--  JWT-->
<dependency>
  <groupId>com.auth0</groupId>
  <artifactId>java-jwt</artifactId>
  <version>3.4.1</version>
</dependency>
<!-- redis 缓存操作 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
 <!-- 阿里JSON解析器 -->
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>fastjson</artifactId>
</dependency>

<!-- spring security 安全认证 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- spring security oauth2 开放授权 -->
<dependency>
  <groupId>org.springframework.security.oauth</groupId>
  <artifactId>spring-security-oauth2</artifactId>
  <version>2.3.5.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.3.5.RELEASE</version>
</dependency>

2.3 添加 @EnableOAuth2Sso

/**
* spring security配置
*
*/
@EnableOAuth2Sso
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    .....
}

@EnableOAuth2Sso注解详情

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties({OAuth2SsoProperties.class}) // [1]
@Import({OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class, ResourceServerTokenServicesConfiguration.class})
public @interface EnableOAuth2Sso {
}

由源码知@EnableOAuth2Sso是一个组合注解。

@EnableOAuth2Client
@EnableOAuth2Client通过@Import导入了OAuth2ClientConfiguration类。

OAuth2ClientConfiguration类中注入了3个Bean:OAuth2ClientContextFilter,AccessTokenRequest和OAuth2ClientContextConfiguration。

OAuth2ClientContextFilter加入到过滤器链中,当后续的认证抛出UserRedirectRequiredException异常时,会重定向。。。

@EnableConfigurationProperties

@EnableConfigurationProperties启用OAuth2SsoProperties配置类。 回调 /login

security.oauth2.sso.loginPath=/login

@Import

导入了OAuth2SsoDefaultConfiguration,OAuth2SsoCustomConfiguration和ResourceServerTokenServicesConfiguration三个配置类。

看OAuth2SsoDefaultConfiguration的条件@Conditional({OAuth2SsoDefaultConfiguration.NeedsWebSecurityCondition.class})和OAuth2SsoCustomConfiguration上的条件@Conditional({EnableOAuth2SsoCondition.class}),它们两个是取反的逻辑,即只能有一个生效。

@Conditional({EnableOAuth2SsoCondition.class})是指当项目中有WebSecurityConfigurerAdapter.class的Bean时为真,即项目中继承了WebSecurityConfigurerAdapter抽象类。

① OAuth2SsoDefaultConfiguration配置类

OAuth2SsoDefaultConfiguration继承了WebSecurityConfigurerAdapter,重写了config(HttpSecurity)方法

protected void configure(HttpSecurity http) throws Exception {
    ((AuthorizedUrl)http.antMatcher("/**").authorizeRequests().anyRequest()).authenticated();
    (new SsoSecurityConfigurer(this.applicationContext)).configure(http);
}

方法中配置了所有的请求都需要认证后访问。另外,调用了SsoSecurityConfigurer的config方法。

SsoSecurityConfigurer的configure

public void configure(HttpSecurity http) throws Exception {
  // 在[1]处已经启用了OAuth2SsoProperties属性类
    OAuth2SsoProperties sso = (OAuth2SsoProperties)this.applicationContext.getBean(OAuth2SsoProperties.class);
  // oauth2SsoFilter方法返回OAuth2ClientAuthenticationProcessingFilter对象
  // OAuth2ClientAuthenticationConfigurer主要作用将OAuth2ClientAuthenticationProcessingFilter添加到过滤器链中
    http.apply(new SsoSecurityConfigurer.OAuth2ClientAuthenticationConfigurer(this.oauth2SsoFilter(sso)));
  // 认证失败的异常处理端点
    this.addAuthenticationEntryPoint(http, sso);
}

SsoSecurityConfigurer的oauth2SsoFilter

private OAuth2ClientAuthenticationProcessingFilter oauth2SsoFilter(OAuth2SsoProperties sso) {
    OAuth2RestOperations restTemplate = ((UserInfoRestTemplateFactory)this.applicationContext.getBean(UserInfoRestTemplateFactory.class)).getUserInfoRestTemplate();
    ResourceServerTokenServices tokenServices = (ResourceServerTokenServices)this.applicationContext.getBean(ResourceServerTokenServices.class);
    OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(sso.getLoginPath());
    filter.setRestTemplate(restTemplate);
    filter.setTokenServices(tokenServices);
    filter.setApplicationEventPublisher(this.applicationContext);
    return filter;
}

方法返回OAuth2ClientAuthenticationProcessingFilter对象,OAuth2ClientAuthenticationProcessingFilterUsernamePasswordAuthenticationFilter一样处理用户认证的逻辑。不同的是UsernamePasswordAuthenticationFilter通过username和password去认证了,而OAuth2ClientAuthenticationProcessingFilter是通过OAuth2RestOperations「restTemplate」取获取accessToken,然后使用accessToken获取认证信息。

SsoSecurityConfigurer.OAuth2ClientAuthenticationConfigurer类

private static class OAuth2ClientAuthenticationConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private OAuth2ClientAuthenticationProcessingFilter filter;

    OAuth2ClientAuthenticationConfigurer(OAuth2ClientAuthenticationProcessingFilter filter) {
        this.filter = filter;
    }

    public void configure(HttpSecurity builder) throws Exception {
        OAuth2ClientAuthenticationProcessingFilter ssoFilter = this.filter;
        ssoFilter.setSessionAuthenticationStrategy((SessionAuthenticationStrategy)builder.getSharedObject(SessionAuthenticationStrategy.class));
      // 将OAuth2ClientAuthenticationProcessingFilter添加到过滤器链中
        builder.addFilterAfter(ssoFilter, AbstractPreAuthenticatedProcessingFilter.class);
    }
}

该类的config方法中为OAuth2ClientAuthenticationProcessingFilter设置了Session策略,然后将其添加到过滤器链中。

② OAuth2SsoCustomConfiguration配置类

该类OAuth2SsoCustomConfiguration作用是:如果项目中实现了WebSecurityConfigurerAdapter的配置类,则为该类使用代理。

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    if (this.configType.isAssignableFrom(bean.getClass()) && bean instanceof WebSecurityConfigurerAdapter) {
      // 使用代理
        ProxyFactory factory = new ProxyFactory();
        factory.setTarget(bean);
        factory.addAdvice(new OAuth2SsoCustomConfiguration.SsoSecurityAdapter(this.applicationContext));
        bean = factory.getProxy();
    }

    return bean;
}

SsoSecurityAdapter

private static class SsoSecurityAdapter implements MethodInterceptor {
    private SsoSecurityConfigurer configurer;

    SsoSecurityAdapter(ApplicationContext applicationContext) {
        this.configurer = new SsoSecurityConfigurer(applicationContext);
    }

    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (invocation.getMethod().getName().equals("init")) {
        // 在调用init方法添加SsoSecurityConfigurer配置类
            Method method = ReflectionUtils.findMethod(WebSecurityConfigurerAdapter.class, "getHttp");
            ReflectionUtils.makeAccessible(method);
            HttpSecurity http = (HttpSecurity)ReflectionUtils.invokeMethod(method, invocation.getThis());
            this.configurer.configure(http);
        }

        return invocation.proceed();
    }
}

ResourceServerTokenServicesConfiguration

这个配置类主要是向容器中添加了UserInfoRestTemplateFactory和JwtToken相关的配置,被用于获取用户信息及token的生成与存储。

2.4 校验令牌方法

/**
* 获取用户身份信息
*
* @return 用户信息
*/
public LoginUser getLoginUser(HttpServletRequest request) {
    // 获取请求携带的令牌
    String token = getToken(request);
    if (StringUtils.isNotEmpty(token)) {
        try {
            //调用授权中心接口进行校验
            Map<String, String> map = new HashMap<>(1);
            map.put("token",token);
            ResponseEntity<AjaxResult> response = RestTemplateUtils.post(tokenProperties.getCheckTokenUrl(),map, AjaxResult.class);
            if (response.getStatusCodeValue() == HttpStatus.HTTP_OK) {
                if(StringUtils.isNotEmpty(response.getBody())){
                    AjaxResult result = response.getBody();
                    OauthCallBackInfo oauthCallBackInfo = JSONObject.parseObject(result.getData().toString(), OauthCallBackInfo.class);
                    //可以对用户信息进行缓存 是否缓存?
                    return oauthCallBackInfo.getLoginUser();
                }
            }else {
                throw new BaseException(response.getBody().toString());
            }
        } catch (Exception e) {
            log.error("获取身份信息失败!" + e.getMessage());
        }
    }
    return null;
}
/**
* 获取请求token
*
* @return token
*/
public String getToken(HttpServletRequest request) {
    String token = request.getHeader(tokenProperties.getHeader());
    // Constants.TOKEN_PREFIX = "Bearer "
    if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) {
        token = token.replace(Constants.TOKEN_PREFIX, "");
    }
    return token;
}
/**
 * 授权回调信息
 * @date 2022/4/7 23:56
 */
@Data
public class OauthCallBackInfo {

    /**
    * 过期时间
    */
    private String expire;
    /**
    * info
    */
    private List<Map<String, Object>> info;
    /**
    * 登录人信息
    */
    private LoginUser loginUser;
}

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-06-01 15:02:20  更:2022-06-01 15:04:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 19:54:45-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码