美文网首页Spring 学习SpringbootSpring Cloud
Springboot security oauth2 jwt实现

Springboot security oauth2 jwt实现

作者: KICHUN | 来源:发表于2019-04-28 15:33 被阅读497次

    为什么使用jwt?

    在原先dubbo+zookeeper项目中,web模块只暴露Restful接口,各服务模块只暴露duboo接口,此时用户登录后由web项目进行token的鉴权和验证,并通过dubbo的隐式传参将sessionID传递给dubbo服务模块, 拦截器再根据sessionID从Redis中获取用户信息设置到当前线程

    然鹅,在springcloud中,各个微服务直接暴露的是restful接口,此时如何让各个微服务获取到当前用户信息呢?最佳的方式就是token了,token作为BS之间的会话标识(一般是原生随机token),同时也可以作为信息的载体传递一些自定义信息(jwt, 即Json web token)。

    为了能更清楚的了解本文,需要对spring-security-oauth 及 jwt有一定了解,本文只关注用户信息传递这一块

    认证服务器

    认证服务器配置AuthorizationServerConfigurerAdapter

    @Configuration
    @PropertySource({"classpath:application.yml"})
    @EnableAuthorizationServer
    public class AuthServerConfiguration extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        AuthenticationManager authenticationManager;
    
        @Autowired
        TokenStore tokenStore;
    
        @Autowired
        JwtAccessTokenConverter jwtAccessTokenConverter;
    
        @Autowired
        ApprovalStore approvalStore;
    
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.jdbc(dataSource());
        }
    
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager)
                    .tokenStore(tokenStore)
                    .accessTokenConverter(jwtAccessTokenConverter)
                    .approvalStore(approvalStore)
                    .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        }
    
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            //允许表单认证
            security
                    .tokenKeyAccess("permitAll()") //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
                    .checkTokenAccess("permitAll()")
                    .allowFormAuthenticationForClients();
        }
    
        @Bean
        @Primary
        @ConfigurationProperties("ms-sql.datasource")
        public DataSource dataSource() {
            return new DriverManagerDataSource();
        }
    
        @Bean
        public ApprovalStore approvalStore() {
            return new JdbcApprovalStore(dataSource());
        }
    
    
        /**
         * 使用 Jwt token
         * @param accessTokenConverter
         * @return
         */
        @Bean
        public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
            return new JwtTokenStore(accessTokenConverter);
        }
    
        /**
         * token 转换器,加入对称秘钥,使用自定tokenEnhancer
         * @return
         */
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();
            converter.setSigningKey("secretKey");
            return converter;
        }
    
    }
    

    自定义token转换器
    CustomJwtAccessTokenConverter

    /**
     * 对JwtAccessTokenConverter 的 enhance进行重写,加入自定义的信息
     *
     * @author wangqichang
     * @since 2019/4/26
     */
    public class CustomJwtAccessTokenConverter extends JwtAccessTokenConverter {
    
        private static final String BEARER_PRIFIX = "bearer ";
    
    //这个是token增强器,想让jwt token携带额外的信息在这里处理
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            if (accessToken instanceof DefaultOAuth2AccessToken) {
                Object principal = authentication.getPrincipal();
    
    //这个principal是当时登录后存到securiy的东东,一般是用户实体,自己debug一下就知道了
                if (principal instanceof OAuthUser) {
                    OAuthUser user = (OAuthUser) principal;
                    HashMap<String, Object> map = new HashMap<>();
    
                    //jwt默认已经自带用户名,无需再次加入
                    map.put("nick_name", user.getUsernickname());
                    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
                }
            }
            return super.enhance(accessToken, authentication);
        }
    
    
    //主要是资源服务器解析时一定要有bearer这个头才认为是一个oauth请求,但不知道为啥指定jwt后这个头就不见了,特意加上去
        @Override
        protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            return BEARER_PRIFIX + super.encode(accessToken, authentication);
        }
    }
    
    

    此时按照固定格式访问授权服务器token接口获取token,如图,可以获取到jwt格式的token,并且额外信息nick_name也已经添加

    image.png

    直接解析jwt字符串可以获取到以下信息,即用户名和授权信息


    image.png

    资源服务器如何鉴权?

    只需要指定和授权服务器一模一样的token store 和token converter
    在securiy的过滤器中OAuth2AuthenticationProcessingFilter会从token中获取相关信息进行鉴权
    源码:

     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
            boolean debug = logger.isDebugEnabled();
            HttpServletRequest request = (HttpServletRequest)req;
            HttpServletResponse response = (HttpServletResponse)res;
    
            try {
    //这里从请求中获取到Authorization的 header
                Authentication authentication = this.tokenExtractor.extract(request);
                if (authentication == null) {
                    if (this.stateless && this.isAuthenticated()) {
                        if (debug) {
                            logger.debug("Clearing security context.");
                        }
    
                        SecurityContextHolder.clearContext();
                    }
    
                    if (debug) {
                        logger.debug("No token in request, will continue chain.");
                    }
                } else {
                    request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                    if (authentication instanceof AbstractAuthenticationToken) {
                        AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
                        needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
                    }
    
    //这里将调用当前设定的token Store 和 converter 将字符串token转成Authentication 
                    Authentication authResult = this.authenticationManager.authenticate(authentication);
                    if (debug) {
                        logger.debug("Authentication success: " + authResult);
                    }
    
                    this.eventPublisher.publishAuthenticationSuccess(authResult);
    //认证成功后,这里将安全上下文设置用户信息,一般包含用户名和权限。额外信息需要自定义处理,security不会帮你处理的
                    SecurityContextHolder.getContext().setAuthentication(authResult);
                }
            } catch (OAuth2Exception var9) {
                SecurityContextHolder.clearContext();
                if (debug) {
                    logger.debug("Authentication request failed: " + var9);
                }
    
                this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
                this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
                return;
            }
    
            chain.doFilter(request, response);
        }
    
    

    注意,资源服务器主要配置在
    ResourceServerConfigurerAdapter

    @Configuration
    @EnableResourceServer
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class ResourceConfig extends ResourceServerConfigurerAdapter {
    
    
        @Autowired
        TokenStore tokenStore;
    
        @Autowired
        JwtAccessTokenConverter jwtAccessTokenConverter;
    
    
        @Bean
        public TokenStore tokenStore(@Autowired JwtAccessTokenConverter accessTokenConverter) {
            return new JwtTokenStore(accessTokenConverter);
        }
    
        @Bean
        public JwtAccessTokenConverter accessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey("secretKey");
            return converter;
        }
    
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setTokenStore(tokenStore);
            resources.tokenServices(defaultTokenServices);
            super.configure(resources);
        }
    
    
    //其他资源不做限制,让security直接放行,这样只限制注解了的方法
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().antMatchers("/**").permitAll();
            super.configure(http);
        }
    }
    
    

    微服务获取jwttoken中的用户信息,两种方式,使用security上下文可以直接获取当前用户名和权限,另一种自定义拦截器获取额外信息。
    这个就简单了,获取header头解析验证token
    然后获取之前从授权服务器中的添加的 nick_name的额外信息放入线程变量

    /**
     * 用户信息拦截器
     * 从Header中取出jwttoken,并获取其中的用户名设置到用户信息上下文线程变量中
     *
     * @author wangqichang
     * @since 2019/4/24
     */
    public class UserInfoInterceptor implements HandlerInterceptor {
        private ObjectMapper objectMapper = new ObjectMapper();
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            String authorization = request.getHeader("Authorization");
            authorization = StrUtil.removePrefix(authorization, "bearer ");
            if (StrUtil.isNotBlank(authorization)) {
                Jwt decode = JwtHelper.decode(authorization);
                //验签
    //            Jwt secretKey = JwtHelper.decodeAndVerify(authorization, new MacSigner("secretKey"));
                String claims = decode.getClaims();
                HashMap<String, Object> hashMap = objectMapper.readValue(claims, HashMap.class);
                Object userName = hashMap.get("user_name");
                Object nickName = hashMap.get("nick_name");
                Object authorities = hashMap.get("authorities");
                UserContext.setUserInfo(UserInfo.builder().userName((String) userName).nickName((String) nickName).build());
            }
            return true;
        }
    }
    

    其中用户上下文类

    /**
     * @author wangqichang
     * @since 2019/4/24
     */
    public class UserContext {
    
        private static ThreadLocal<UserInfo> threadLocal = new ThreadLocal<>();
    
        public static UserInfo current() {
            return threadLocal.get();
        }
    
        public static String currentUserName() {
            UserInfo userInfo = threadLocal.get();
            if (ObjectUtil.isNotNull(userInfo)) {
                return userInfo.getUserName();
            }
            return null;
        }
    
        public static void setUserInfo(UserInfo userInfo) {
            threadLocal.set(userInfo);
        }
    }
    
    

    启动拦截器注册webmvc配置类

    /**
     * @author wangqichang
     * @since 2019/4/25
     */
    @Configuration
    public class CustomWebMvcConfig extends WebMvcConfigurationSupport {
    
        /**
         * 注册用户信息拦截器
         *
         * @param registry
         */
        @Override
        protected void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new UserInfoInterceptor()).addPathPatterns("/**");
            super.addInterceptors(registry);
        }
    
        /**
         * 当自定义webmvc配置后,swagger无法读取到静态Resouce资源,需要手动添加
         *
         * @param registry
         */
        @Override
        protected void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("swagger-ui.html")
                    .addResourceLocations("classpath:/META-INF/resources/");
            registry.addResourceHandler("/webjars/**")
                    .addResourceLocations("classpath:/META-INF/resources/webjars/");
            super.addResourceHandlers(registry);
        }
    }
    

    在controller中获取用户信息如图

    image.png

    自定义Oauth异常信息

    在默认的认证异常如图


    image.png

    假设我们做了全局异常处理,前端希望在token过期时做统一的登录跳转如何做?
    实现AuthenticationEntryPoint接口重写commence方法即可
    注意,直接抛出异常并不会走@RestControllerAdvice, 因为在这里是response直接返回,并没有使用到Controller处理

    /**
     * @author wangqichang
     * @since 2019/4/30
     */
    public class CustomOAuth2AuthenticationEntryPoint extends AbstractOAuth2SecurityExceptionHandler implements AuthenticationEntryPoint {
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    
            Response<Object> build = Response.builder().respCode(StatusCode.TOKEN_ERR).respDesc(authException.getMessage()).build();
            response.setContentType("application/json");
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write(objectMapper.writeValueAsString(build));
    
        }
    
        @ConditionalOnMissingBean
        public ObjectMapper objectMapper() {
            return new ObjectMapper();
        }
    }
    

    此时返回我自定义的Response对象,如图


    image.png

    相关文章

      网友评论

        本文标题:Springboot security oauth2 jwt实现

        本文链接:https://www.haomeiwen.com/subject/pwldnqtx.html