您现在的位置是:首页 >技术教程 >Spring Security + oauth2一文整合网站首页技术教程

Spring Security + oauth2一文整合

更容易记住我 2025-02-17 12:01:03
简介Spring Security + oauth2一文整合

一、授权认证配置类

  • CommonAuthorizationServerConfigurerAdapter继承AuthorizationServerConfigurerAdapter
  • 添加认证服务注解:@EnableAuthorizationServer
  • 复写三个方法
    • configure(ClientDetailsServiceConfigurer clients)
      • 配置oauth2的客户端相关信息
        • 内存保存客户端信息
        • jdbc数据库保存
          • 创建EduClientDetailService数据库连接处理类

          • 因为clientID每个程序都是唯一的,但是我们要针对不同系统,使用不同的clientID

            • 因此需要在EduClientDetailService 中复写loadClientByClientId方法,查询出来对应的clientDetail
            
            package com.cloud.edu.common.security.service;
            
            import com.cloud.edu.common.core.constant.SecurityConstants;
            import lombok.SneakyThrows;
            import org.springframework.cache.annotation.Cacheable;
            import org.springframework.security.oauth2.provider.ClientDetails;
            import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
            
            import javax.sql.DataSource;
            
            /**
             * 将客户端连接信息保存到数据库
             * see JdbcClientDetailsService
             */
            public class EduClientDetailService extends JdbcClientDetailsService {
            
                public EduClientDetailService(DataSource dataSource) {
                    super(dataSource);
                }
            
                /**
                 * 重写原生方法支持redis缓存写原生方法支持redis缓存
                 *
                 * @param clientId
                 * @return
                 */
                @Override
                @SneakyThrows
                @Cacheable(value = SecurityConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
                public ClientDetails loadClientByClientId(String clientId) {
                    return super.loadClientByClientId(clientId);
                }
            }
            
            
            • 生成固定的数据库,执行oauth_client_details.sql文件即可
            DROP TABLE IF EXISTS `oauth_client_details`;
            CREATE TABLE `oauth_client_details` (
              `client_id` varchar(48) NOT NULL,
              `resource_ids` varchar(256) DEFAULT NULL,
              `client_secret` varchar(256) DEFAULT NULL,
              `scope` varchar(256) DEFAULT NULL,
              `authorized_grant_types` varchar(256) DEFAULT NULL,
              `web_server_redirect_uri` varchar(256) DEFAULT NULL,
              `authorities` varchar(256) DEFAULT NULL,
              `access_token_validity` int(11) DEFAULT NULL,
              `refresh_token_validity` int(11) DEFAULT NULL,
              `additional_information` varchar(4096) DEFAULT NULL,
              `autoapprove` varchar(256) DEFAULT NULL,
              PRIMARY KEY (`client_id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
            
          • 在数据库中根据自身业务添加对应的数据信息

            • 例如:后台管理端登录,比较简易的
              • authorized_grant_types:password,refresh_token
              • scope:all
              • client_id:manager
              • client_secret:123456
              • autoapprove:true
            • 这些数据authorized_grant_types是要根据认证类型来的
    • configure(AuthorizationServerSecurityConfigurer security)
      • 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)安全策略
    • configure(AuthorizationServerEndpointsConfigurer endpoints)
      • 配置令牌端点(Token Endpoint)的安全约束端点
      • 配置token的存储方式
        • InMemoryTokenStore(默认)

          • 内存存储模型,并发要求较低模式下可以选择,无需任何配置,使用默认的tokenStore注入即可
           @Autowired private TokenStore InMemoryTokenStore;    endpoints.tokenStore(InMemoryTokenStore); 
          
        • JdbcTokenStore

          • 自定义jdbcToken即可
          @Bean
          public TokenStore jdbcTokenStore(DataSource dataSource) {
              return new JdbcTokenStore(dataSource);
          }
          
          @Autowired private TokenStore jdbcTokenStore;    
          endpoints.tokenStore(jdbcTokenStore);
          
        • JwtTokenStore

          • JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的
          • 配置JwtTokenStore ,并向token添加jwtSigningKey和自定义的相关信息
          @Bean
          public JwtTokenStore jwtTokenStore() {
          	return new JwtTokenStore(accessTokenConverter());
          }
          
          @Bean
          public JwtAccessTokenConverter accessTokenConverter() {
              CommonJwtTokenEnhancer converter = new CommonJwtTokenEnhancer();
              converter.setSigningKey(“jwtSigningKey”);
              return converter;
          }
          
           @Autowired private TokenStore jwtTokenStore;    
           @Autowired private JwtAccessTokenConverter jwtAccessTokenConverter;
           
           endpoints.tokenStore(jwtTokenStore).accessTokenConverter(jwtAccessTokenConverter);
          
        • RedisTokenStore

          private final RedisConnectionFactory redisConnectionFactory;
          
          @Bean
          public TokenStore tokenStore() {
              RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
              tokenStore.setPrefix(SecurityConstants.PROJECT_PREFIX + SecurityConstants.OAUTH_PREFIX);
              return tokenStore;
          }
          

二、资源授权配置类

  • CommonResourceServerAdapter继承ResourceServerConfigurerAdapter
package com.zcgk.xmdj.web.config;

import com.zcgk.xmdj.service.constants.SystemConstants;
import com.zcgk.xmdj.web.handler.CommonAccessDeniedHandler;
import com.zcgk.xmdj.web.handler.CommonAuthenticationEntryPointHandler;
import com.zcgk.xmdj.web.jwt.CommonJwtAuthenticationFilter;
import com.zcgk.xmdj.web.security.CommonAccessDecisionManager;
import com.zcgk.xmdj.web.security.CommonSecurityMetadataSourceManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;

import java.util.Arrays;
import java.util.List;

/**
 * 资源权限认证类
 */
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled = true)  //使用表达式时间方法级别的安全性 && 过滤权限
public class CommonResourceServerAdapter extends ResourceServerConfigurerAdapter {
    //资源认证在一个服务器
    @Autowired private DefaultTokenServices tokenServices;
    //资源认证在两个服务器,需要remoteTokenServices远程认证token获取用户信息
    @Autowired private RemoteTokenServices remoteTokenServices;

    @Autowired private JwtTokenStore tokenStore;
    //用来根据URL动态获取资源权限
    @Autowired private CommonSecurityMetadataSourceManager securityMetadataSourceManager;
    //资源认证管理-用来认证资源是否允许访问
    @Autowired private CommonAccessDecisionManager accessDecisionManager;
    @Autowired private CommonAuthenticationEntryPointHandler commonAuthenticationEntryPointHandler;
    @Autowired private CommonAccessDeniedHandler commonAccessDeniedHandler;


    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        /**
         *  如果授权和认证不在一个服务器,可以使用remoteTokenServices进行远程认证
         *  在授权服务器调用认证服务器获取token并获取用户信息
         *  remoteTokenServices 最终会主席那个loadAuthentication方法
         *      return this.tokenConverter.extractAuthentication(map);
         *          tokenConverter就是注入的token增强器
         *
         *      1、我们使用DefaultAccessTokenConverter  并且添加上自定义的UserTokenConverter
         *          1.1  remoteTokenServices 会执行DefaultAccessTokenConverter  类的extractAuthentication方法
         *          1.2 extractAuthentication方法中会执行 extractAuthentication方法
         *              1.2.1 EduUserAuthenticationConverter实现UserAuthenticationConverter 并重写 extractAuthentication方法,实现自身逻辑
         *               -- extractAuthentication方法  的参数 Map<String, ?> map 就是我们的 Authentication对象的Map表现形式
         *                 包括:自定义的用户信息(通常有用户名、编号等自己添加到token中的信息以及 authorities权限信息)
         *
         */
//        DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter();
//        UserAuthenticationConverter userTokenConverter = new EduUserAuthenticationConverter();
//        accessTokenConverter.setUserTokenConverter(userTokenConverter);
//        //认证地址 auth 是认证微服务的名字   /oauth/check_token 是固定的接口api
//        remoteTokenServices.setCheckTokenEndpointUrl("http://auth/oauth/check_token");
//        remoteTokenServices.setAccessTokenConverter(accessTokenConverter);
//        remoteTokenServices.setClientId("client-id");
//        remoteTokenServices.setClientSecret("client-secret");

        resources.accessDeniedHandler(commonAccessDeniedHandler)
                .tokenServices(tokenServices)
//                .tokenServices(remoteTokenServices)
                .authenticationEntryPoint(commonAuthenticationEntryPointHandler);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        List<String> permitAllEndpointList = Arrays.asList(
                SystemConstants.API_TOKEN_URL, SystemConstants.API_USER_INFO
        );
        CommonJwtAuthenticationFilter jwtFilter = new CommonJwtAuthenticationFilter(tokenStore); //添加token解析拦截器,贯穿用户信息到线程上下文
        http.authorizeRequests()
                .antMatchers(permitAllEndpointList.toArray(new String[permitAllEndpointList.size()])).permitAll()//不需要权限的接口
                .anyRequest().authenticated() //所有请求都需要权限
                //自定义配置FilterSecurityInterceptor中的对象信息
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(accessDecisionManager);
                        object.setSecurityMetadataSource(securityMetadataSourceManager);
                        return object;
                    }
                })
        .and().addFilterBefore(jwtFilter, FilterSecurityInterceptor.class);
    }

}

  • 添加授权服务注解:@EnableResourceServer
  • 复写两个方法
    • configure(ResourceServerSecurityConfigurer resources)
      • 主要用于配置认证授权处理类
    • configure(HttpSecurity http)
      • 主要增加一些资源认证过滤器、免权限接口等
  • 如果资源和认证不在一个服务器,需要使用remoteTokenServices进行认证远程调用
    • 在认证服务器端设置对应的Client信息
    • 认证服务器默认有一个/oauth/check_token的认证access_token的接口
    • 具体流程
      • remoteTokenServices类中会执行loadAuthentication方法
        • loadAuthentication方法中执行了认证的远程调用
        • 并将返回的用户信息的map对象,调用DefaultAccessTokenConverter的extractAuthentication方法
      • extractAuthentication方法中 this.userTokenConverter.extractAuthentication(map)
        • userTokenConverter就是注入的UserAuthenticationConverter
          • 我们自定义MyUserAuthenticationConverter类继承UserAuthenticationConverter
            • 复写extractAuthentication方法,方法的参数map就是我们在客户端定义的增强对象
              • map中包含系统内置的authorities、user_name以及自定义用户的属性


package com.cloud.edu.common.security.component;

import com.cloud.edu.common.security.service.EduUser;
import com.cloud.edu.common.security.service.EduUserDetailServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * <p>
 * 根据checktoken 的结果转化用户信息
 */
public class MyUserAuthenticationConverter implements UserAuthenticationConverter {

    private static final String USER_ID = "userId";
    private static final String Name = "name";
    private static final String User_No = "userNo";
    private static final String N_A = "N/A";

    /**
     * Extract information about the user to be used in an access token (i.e. for resource servers).
     *
     * @param authentication an authentication representing a user
     * @return a map of key values representing the unique information about the user
     */
    @Override
    public Map<String, ?> convertUserAuthentication(Authentication authentication) {
        Map<String, Object> response = new LinkedHashMap<>();
        response.put(USERNAME, authentication.getName());
        if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
        }
        return response;
    }

    /**
     * Inverse of {@link #convertUserAuthentication(Authentication)}. Extracts an Authentication from a map.
     *
     * @param map a map of user information
     * @return an Authentication representing the user or null if there is none
     */
    @Override
    public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            String username = (String) map.get(USERNAME);
            Integer id = (Integer) map.get(USER_ID);
            Integer userNo = (Integer) map.get(User_No);
            EduUser user = new EduUser(id, userNo, username, N_A, true
                    , true, true, true, authorities);
            return new UsernamePasswordAuthenticationToken(user, N_A, authorities);
        }
        return null;
    }

    @Autowired
    private UserDetailsService userDetailsService;

    private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
        Object authorities = map.get(AUTHORITIES);
        if (authorities instanceof String) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
        }
        if (authorities instanceof Collection) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils
                    .collectionToCommaDelimitedString((Collection<?>) authorities));
        }
        if (authorities == null) {
            if (this.userDetailsService != null) {
                UserDetails user = this.userDetailsService.loadUserByUsername((String) map.get("user_name"));
                return user.getAuthorities();
            }
        }
        throw new IllegalArgumentException("Authorities must be either a String or a Collection");
    }
}

三、token增强器

  • CommonJwtTokenEnhancer继承JwtAccessTokenConverter
  • 复写三个方法
    • enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
      • 主要方法:添加自定义信息(用户信息)到token中
    • extractAccessToken(String value, Map<String, ?> map)
    • extractAuthentication(Map<String, ?> claims)
      • 添加用户信息到OAuth2Authentication对象
package com.zcgk.xmdj.web.jwt;

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.store.JwtAccessTokenConverter;

import java.util.HashMap;
import java.util.Map;

/**
 * jwt增强器,将jwt转化为token
 */
public class CommonJwtTokenEnhancer extends JwtAccessTokenConverter {

    /**
     * 添加自定义信息到token中
     * @param accessToken
     * @param authentication
     * @return
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> tokenExtra = new HashMap<>();
        CommonJwtUser user = (CommonJwtUser) authentication.getUserAuthentication().getPrincipal();
            tokenExtra.put("userNo", user.getUserNo());
            tokenExtra.put("name", user.getName());
            tokenExtra.put("userId", user.getUserId());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(tokenExtra);
        accessToken = super.enhance(accessToken, authentication);
        return accessToken;
    }
    /**
     * 解析token
     * @param value
     * @param map
     * @return
     */
    @Override
    public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
        OAuth2AccessToken oauth2AccessToken = super.extractAccessToken(value, map);
        Map<String, Object> additionInfoMap = oauth2AccessToken.getAdditionalInformation();
        return oauth2AccessToken;
    }

    /**
     * 设置用户信息到对象信息
     * @param claims
     * @return
     */
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
        OAuth2Authentication authentication = super.extractAuthentication(claims);
        authentication.setDetails(claims);
        return authentication;
    }
}

四、token解析拦截器

  • CommonJwtAuthenticationFilter继承OncePerRequestFilter
    • OncePerRequestFilter:通过request.getAttribute判断当前过滤器是否已执行,保证只会被执行一次
  • 复写doFilterInternal方法
    • 通过request获取用户对象:Authentication authentication = tokenExtractor.extract(request)
    • 通过用户对象获取token:String tokenValue = authentication.getPrincipal().toString();
    • 读取用户信息:OAuth2Authentication oauth = tokenStore.readAuthentication(tokenValue);
    • 用户信息:Map<String, String> userInfoMap = (Map<String, String>) oauth.getDetails();
package com.zcgk.xmdj.web.jwt;

import com.zcgk.xmdj.service.constants.ContextUserDto;
import com.zcgk.xmdj.service.constants.ContextUserLocal;
import com.zcgk.xmdj.service.constants.SystemConstants;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import lombok.Setter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.authentication.BearerTokenExtractor;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;

/**
 * 拦截器会按照链式顺序依次执行doFilter()方法
 * 只拦截一次过滤,获取登陆的用户信息,并放入上下文中
 * OncePerRequestFilter.doFilter方法中通过request.getAttribute判断当前过滤器是否已执行
 * 若未执行过,则调用doFilterInternal方法,交由其子类实现
 */
public class CommonJwtAuthenticationFilter extends OncePerRequestFilter {

    @Setter
    private JwtTokenStore tokenStore;

    //分离出请求中包含的token
    private final BearerTokenExtractor tokenExtractor = new BearerTokenExtractor();

    public CommonJwtAuthenticationFilter(JwtTokenStore tokenStore) {
        this.tokenStore = tokenStore;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
       //判断是否是跨域请求
        if(SystemConstants.METHOD_OPTION.equals(request.getMethod())) {
            return;
        }
        //提取token
        Authentication authentication = tokenExtractor.extract(request);
        if(Checker.BeNull(authentication)) {
            throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
        }

        String tokenValue = authentication.getPrincipal().toString();
        OAuth2Authentication oauth = tokenStore.readAuthentication(tokenValue);//读取用户信息
        if (Checker.BeNull(oauth)) {
            throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
        }
        Map<String, String> userInfoMap = (Map<String, String>) oauth.getDetails();
        if(Checker.BeNull(userInfoMap.get(SystemConstants.USER_ID))) {
            throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
        }

       // 设置用户信息到上下文
        ContextUserDto user = new ContextUserDto();
            user.setUserId(userInfoMap.get(SystemConstants.USER_ID))
            .setName(userInfoMap.get(SystemConstants.USER_NAME));
        ContextUserLocal.put(user);
        //分布式环境下放到session中,seesion保存到redis中
        request.getSession().setAttribute("userinfo", user);
        try {
            //他的作用是将请求转发给过滤器链上下一个filter,如果没有filter那就是你请求的资源。
            filterChain.doFilter(request, response);
        } finally {
            ContextUserLocal.remove();
        }
    }
}

五、登陆认证成功处理类(security)

  • MyAuthenticationSuccessHandler继承SavedRequestAwareAuthenticationSuccessHandler
    • 在WebSecurityConfig中可以绑定successHandler()
    • 复写onAuthentication
    • 可以获取到当前登录的Authentication对象,根据相关信息进行路由跳转或者其他响应信息
    • 可以返回菜单,前端拿到菜单直接跳转到对应的菜单
    • 可以返回完整的地址,或者第三方地址等
    • 原始的mvc模式可以直接redirict重定向页面
package com.zcgk.xmdj.web.security;

import com.alibaba.fastjson.JSON;
import com.zcgk.xmdj.web.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登录成功处理类
 * @author zzx
 * @version 1.0.0
 */
@Slf4j
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        log.info("登录成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.result()));

    }
}

六、登陆认证失败处理类(security)

  • MyAuthenticationFailHandler继承SimpleUrlAuthenticationFailureHandler
  • 在WebSecurityConfig中可以绑定failerHandler()
  • 复写onAuthenticationFailure,可以获取到认证失败的Exception
  • 可以返回对应的错误信息或者跳转连接
package com.zcgk.xmdj.web.security;

import com.alibaba.fastjson.JSON;
import com.zcgk.xmdj.web.common.ResponseResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 登陆失败的异常处理类
 * @author zzx
 * @version 1.0.0
 */
@Slf4j
@Component
public class AuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.info("登录失败");
        //将 登录失败 信息打包成json格式返回
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResponseResult.result()));
    }

}

七、登录结果事件监听(security)

  • 处理两个登录的处理类,security还提供了两个登录的事件
    • AuthenticationSuccessEvent和AbstractAuthenticationFailureEvent
      • 例如: AuthenticationFailureEvenHandler implements ApplicationListener
      • 创建一个事件监听处理类,实现监听器,绑定监听事件即可
        • 可以获取认证后的对象:(Authentication) authenticationSuccessEvent.getSource();
        • 可以获取认证后的异常:AuthenticationException authenticationException = event.getException();

八、已认证但无权限处理类(security)

  • CommonAccessDeniedHandler实现AccessDeniedHandler(OAuth2AccessDeniedHandler)
  • 返回暂无权限状态码:403
  • 抛出暂无资源权限
package com.zcgk.xmdj.web.security;

import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSONObject;
import com.cloud.edu.common.core.constant.CommonConstants;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.common.ResponseResult;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 已验证但无权限处理类
 */

@Component
public class CommonAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
       if(Checker.BeNull(response)) {
           throw new ClientException("response can't be null");
       }
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpStatus.HTTP_FORBIDDEN);
        response.getWriter().write(JSONObject.toJSONString(accessDeniedException.getMessage()));
        ResponseResult.result(ClientExceptionEnum.RESOURCE_VERIFY_FAIL.getCode(), JSONObject.toJSONString(accessDeniedException.getMessage()));
        throw new ClientException(ClientExceptionEnum.RESOURCE_VERIFY_FAIL);

    }
}

九、未认证的无权限处理类(security)

  • CommonAuthenticationEntryPointHandler实现AuthenticationEntryPoint
  • 返回认证失败状态码:401
  • 抛出认证失败,token无效、
package com.zcgk.xmdj.web.security;

import com.alibaba.fastjson.JSONObject;
import com.zcgk.xmdj.utils.Checker;
import com.zcgk.xmdj.web.common.ResponseResult;
import com.zcgk.xmdj.web.exception.ClientException;
import com.zcgk.xmdj.web.exception.ClientExceptionEnum;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 未认证的无权限处理类
 */

@Component
public class CommonAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        if(Checker.BeNull(response)) {
            throw new ClientException("response can't be null");
        }
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write(JSONObject.toJSONString(authException.getMessage()));
        ResponseResult.result(ClientExceptionEnum.INVALID_TOKEN.getCode(), JSONObject.toJSONString(authException.getMessage()));
        throw new ClientException(ClientExceptionEnum.INVALID_TOKEN);
    }
}

十、跨域处理器

  • 端口、协议、ip任意一点不同都会存在跨域问题
package com.zcgk.xmdj.web.config;

import com.google.common.collect.ImmutableList;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import reactor.core.publisher.Mono;

import javax.servlet.ServletException;

/**
 * 跨域请求过滤器
 */
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CommonCorsFilter extends CorsFilter {

    public CommonCorsFilter() {
        super(configurationSource());
    }

    private static UrlBasedCorsConfigurationSource configurationSource() {
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowedOrigins(ImmutableList.of("*"));
        config.setAllowedHeaders(ImmutableList.of("*"));
        config.setAllowedMethods(ImmutableList.of("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"));
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

十一、Feign进行远程调用,请求头token的问题

  • 当我们带着请求头token去调用A服务的时候,可能会存在服务与服务之间的远程调用
    • 调用B服务(也会进行token的校验),因此需要使用feign传递token
  • oauth2提供了一份feign的拦截器:OAuth2FeignRequestInterceptor
    • 拦截器调用apply方法从oAuth2ClientContext中获取token,
    • AccessTokenContextRelay类通过copyToken方法从上下文张获取Authentication对象,并解析出来token放入上下文中
    • 所面临的问题
      • 如果一个feign请求不需要token或者还没有token生成的阶段,如果copyToken,则会造成错误
      • 解决方案:重写apply方法,判断请求的类型
        • apply方法的参数RequestTemplate,就是将要构建的请求对象模板
        • 不需要token的可以在请求头添加自定义的信息,例如以下:
          • 可以从模板的请求头中获取from的标志对象头
            • Collection fromHeader = template.headers().get(“from”);
            • 如果from请求头中包含的是Y,则代表是内部调用接口,也就是不需要token的接口,直接return,无需copytoken
      • 具体代码
        • 创建配置类EduFeignClientInterceptor继承OAuth2FeignRequestInterceptor,复写apply方法


package com.cloud.edu.common.security.feign;

import cn.hutool.core.collection.CollUtil;
import com.cloud.edu.common.core.constant.SecurityConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.security.oauth2.client.AccessTokenContextRelay;
import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

import java.util.Collection;

/**
 * 扩展OAuth2FeignRequestInterceptor
 */
@Slf4j
public class EduFeignClientInterceptor extends OAuth2FeignRequestInterceptor {
    private final OAuth2ClientContext oAuth2ClientContext;
    private final AccessTokenContextRelay accessTokenContextRelay;

    /**
     * Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
     * within Authorization header
     *
     * @param oAuth2ClientContext     provided context
     * @param resource                type of resource to be accessed
     * @param accessTokenContextRelay
     */
    public EduFeignClientInterceptor(OAuth2ClientContext oAuth2ClientContext
            , OAuth2ProtectedResourceDetails resource, AccessTokenContextRelay accessTokenContextRelay) {
        super(oAuth2ClientContext, resource);
        this.oAuth2ClientContext = oAuth2ClientContext;
        this.accessTokenContextRelay = accessTokenContextRelay;
    }


    /**
     * Create a template with the header of provided name and extracted extract
     * 1. 如果使用 非web 请求,header 区别
     * 2. 根据authentication 还原请求token
     *
     * @param template
     */
    @Override
    public void apply(RequestTemplate template) {
        Collection<String> fromHeader = template.headers().get(SecurityConstants.FROM);
        if (CollUtil.isNotEmpty(fromHeader) && fromHeader.contains(SecurityConstants.FROM_IN)) {
            return;
        }

        accessTokenContextRelay.copyToken();
        if (oAuth2ClientContext != null && oAuth2ClientContext.getAccessToken() != null) {
            super.apply(template);
        }
    }
}



package com.cloud.edu.common.security.feign;

import feign.RequestInterceptor;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.security.oauth2.client.AccessTokenContextRelay;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;

/**
 * feign 拦截器传递 header 中oauth token,
 * 使用hystrix 的信号量模式       hytrix隔离策略是thread,无法读到 threadLocal变量
 */
@Configuration
@AllArgsConstructor
@ConditionalOnProperty("security.oauth2.client.client-id")
public class EduFeignClientConfiguration {
    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
                                                            OAuth2ProtectedResourceDetails resource,
                                                            AccessTokenContextRelay accessTokenContextRelay) {
        return new EduFeignClientInterceptor(oAuth2ClientContext, resource, accessTokenContextRelay);
    }
}

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。