• SpringBoot整合SpringSecurity实现JWT认证


    前言

    微服务架构,前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(APP、小程序、H5页面等)通过调用后端的API接口,提交及返回JSON数据进行交互。
    在前后端分离项目中,首先要解决的就是登录及授权的问题。微服务架构下,传统的session认证限制了应用的扩展能力,无状态的JWT认证方法应运而生,该认证机制特别适用于分布式站点的单点登录(SSO)场景

    目录

    该文会通过创建SpringBoot项目整合SpringSecurity,实现完整的JWT认证机制,主要步骤如下:

    1. 创建SpringBoot工程
    2. 导入SpringSecurity与JWT的相关依赖
    3. 定义SpringSecurity需要的基础处理类
    4. 构建JWT token工具类
    5. 实现token验证的过滤器
    6. SpringSecurity的关键配置
    7. 编写Controller进行测试

    1、创建SpringBoot工程

    SpringBoot工程项目目录

    2、导入SpringSecurity与JWT的相关依赖

    pom文件加入以下依赖

     		<!--Security框架-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            ...
            <!-- jwt -->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-api</artifactId>
                <version>0.10.6</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-impl</artifactId>
                <version>0.10.6</version>
            </dependency>
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt-jackson</artifactId>
                <version>0.10.6</version>
            </dependency>
            
    

    3.定义SpringSecurity需要的基础处理类

    application.yml配置中加入jwt配置信息:

    #jwt
    jwt:
      header: Authorization
      # 令牌前缀
      token-start-with: Bearer
      # 使用Base64对该令牌进行编码
      base64-secret: XXXXXXXXXXXXXXXX(制定您的密钥)
      # 令牌过期时间 此处单位/毫秒 
      token-validity-in-seconds: 14400000
    

    创建一个jwt的配置类,并注入Spring,便于程序中灵活调用

    @Data
    @Configuration
    @ConfigurationProperties(prefix = "jwt")
    public class JwtSecurityProperties {
    
        /** Request Headers : Authorization */
        private String header;
    
        /** 令牌前缀,最后留个空格 Bearer */
        private String tokenStartWith;
    
        /** Base64对该令牌进行编码 */
        private String base64Secret;
    
        /** 令牌过期时间 此处单位/毫秒 */
        private Long tokenValidityInSeconds;
    
        /**返回令牌前缀 */
        public String getTokenStartWith() {
            return tokenStartWith + " ";
        }
    }
    

    定义无权限访问类

    @Component
    public class JwtAccessDeniedHandler implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage());
        }
    }
    

    定义认证失败处理类

    @Component
    public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request,
                             HttpServletResponse response,
                             AuthenticationException authException) throws IOException {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException==null?"Unauthorized":authException.getMessage());
        }
    }
    

    4. 构建JWT token工具类

    工具类实现创建token与校验token功能

    @Slf4j
    @Component
    public class JwtTokenUtils implements InitializingBean {
    
        private final JwtSecurityProperties jwtSecurityProperties;
        private static final String AUTHORITIES_KEY = "auth";
        private Key key;
    
        public JwtTokenUtils(JwtSecurityProperties jwtSecurityProperties) {
            this.jwtSecurityProperties = jwtSecurityProperties;
        }
    
        @Override
        public void afterPropertiesSet() {
    
            byte[] keyBytes = Decoders.BASE64.decode(jwtSecurityProperties.getBase64Secret());
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
    
        public  String createToken (Map<String, Object> claims) {
            return Jwts.builder()
                    .claim(AUTHORITIES_KEY, claims)
                    .setId(UUID.randomUUID().toString())
                    .setIssuedAt(new Date())
                    .setExpiration(new Date((new Date()).getTime() + jwtSecurityProperties.getTokenValidityInSeconds()))
                    .compressWith(CompressionCodecs.DEFLATE)
                    .signWith(key,SignatureAlgorithm.HS512)
                    .compact();
        }
    
        public Date getExpirationDateFromToken(String token) {
            Date expiration;
            try {
                final Claims claims = getClaimsFromToken(token);
                expiration = claims.getExpiration();
            } catch (Exception e) {
                expiration = null;
            }
            return expiration;
        }
    
        public Authentication getAuthentication(String token) {
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .parseClaimsJws(token)
                    .getBody();
    
            Collection<? extends GrantedAuthority> authorities =
                    Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                            .map(SimpleGrantedAuthority::new)
                            .collect(Collectors.toList());
    
            HashMap map =(HashMap) claims.get("auth");
    
            User principal = new User(map.get("user").toString(), map.get("password").toString(), authorities);
    
            return new UsernamePasswordAuthenticationToken(principal, token, authorities);
        }
    
        public boolean validateToken(String authToken) {
            try {
                Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
                return true;
            } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
                log.info("Invalid JWT signature.");
                e.printStackTrace();
            } catch (ExpiredJwtException e) {
                log.info("Expired JWT token.");
                e.printStackTrace();
            } catch (UnsupportedJwtException e) {
                log.info("Unsupported JWT token.");
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                log.info("JWT token compact of handler are invalid.");
                e.printStackTrace();
            }
            return false;
        }
    
        private Claims getClaimsFromToken(String token) {
            Claims claims;
            try {
                claims = Jwts.parser()
                        .setSigningKey(key)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                claims = null;
            }
            return claims;
        }
    }
    
    

    5.实现token验证的过滤器

    该类继承OncePerRequestFilter,顾名思义,它能够确保在一次请求中只通过一次filter
    该类使用JwtTokenUtils工具类进行token校验

    @Component
    @Slf4j
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
        private JwtTokenUtils jwtTokenUtils;
    
        public JwtAuthenticationTokenFilter(JwtTokenUtils jwtTokenUtils) {
            this.jwtTokenUtils = jwtTokenUtils;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            JwtSecurityProperties jwtSecurityProperties = SpringContextHolder.getBean(JwtSecurityProperties.class);
            String requestRri = httpServletRequest.getRequestURI();
            //获取request token
            String token = null;
            String bearerToken = httpServletRequest.getHeader(jwtSecurityProperties.getHeader());
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtSecurityProperties.getTokenStartWith())) {
                token = bearerToken.substring(jwtSecurityProperties.getTokenStartWith().length());
            }
    
            if (StringUtils.hasText(token) && jwtTokenUtils.validateToken(token)) {
                Authentication authentication = jwtTokenUtils.getAuthentication(token);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestRri);
            } else {
                log.debug("no valid JWT token found, uri: {}", requestRri);
            }
            filterChain.doFilter(httpServletRequest, httpServletResponse);
    
        }
    }
    

    根据SpringBoot官方让重复执行的filter实现一次执行过程的解决方案,参见官网地址:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-disable-registration-of-a-servlet-or-filter
    在SpringBoot启动类中,加入以下代码:

        @Bean
        public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) {
            FilterRegistrationBean registration = new FilterRegistrationBean(filter);
            registration.setEnabled(false);
            return registration;
        }
    

    6. SpringSecurity的关键配置

    SpringBoot推荐使用配置类来代替xml配置,该类中涉及了以上几个bean来供security使用

    • JwtAccessDeniedHandler :无权限访问
    • jwtAuthenticationEntryPoint :认证失败处理
    • jwtAuthenticationTokenFilter :token验证的过滤器
    package com.zhuhuix.startup.security.config;
    
    import com.fasterxml.jackson.core.filter.TokenFilter;
    import com.zhuhuix.startup.security.security.JwtAccessDeniedHandler;
    import com.zhuhuix.startup.security.security.JwtAuthenticationEntryPoint;
    import com.zhuhuix.startup.security.security.JwtAuthenticationTokenFilter;
    import com.zhuhuix.startup.security.utils.JwtTokenUtils;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.web.DefaultSecurityFilterChain;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    /**
     * Spring Security配置类
     *
     * @author zhuhuix
     * @date 2020-03-25
     */
    
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
        private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
        private final JwtTokenUtils jwtTokenUtils;
    
        public WebSecurityConfig(JwtAccessDeniedHandler jwtAccessDeniedHandler, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtTokenUtils jwtTokenUtils) {
    
            this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
            this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
            this.jwtTokenUtils = jwtTokenUtils;
    
        }
    
        @Override
        protected void configure(HttpSecurity httpSecurity) throws Exception {
    
            httpSecurity
                    // 禁用 CSRF
                    .csrf().disable()
    
                    // 授权异常
                    .exceptionHandling()
                    .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                    .accessDeniedHandler(jwtAccessDeniedHandler)
    
                    // 防止iframe 造成跨域
                    .and()
                    .headers()
                    .frameOptions()
                    .disable()
                    
    				// 不创建会话
                    .and()
                    .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    
                    .and()
                    .authorizeRequests()
    
                    // 放行静态资源
                    .antMatchers(
                            HttpMethod.GET,
                            "/*.html",
                            "/**/*.html",
                            "/**/*.css",
                            "/**/*.js",
                            "/webSocket/**"
                    ).permitAll()
    
                    // 放行swagger
                    .antMatchers("/swagger-ui.html").permitAll()
                    .antMatchers("/swagger-resources/**").permitAll()
                    .antMatchers("/webjars/**").permitAll()
                    .antMatchers("/*/api-docs").permitAll()
    
                    // 放行文件访问
                    .antMatchers("/file/**").permitAll()
    
                    // 放行druid
                    .antMatchers("/druid/**").permitAll()
    
                    // 放行OPTIONS请求
                    .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
    
                    //允许匿名及登录用户访问
                    .antMatchers("/api/auth/**", "/error/**").permitAll()
                    // 所有请求都需要认证
                    .anyRequest().authenticated();
    
            // 禁用缓存
            httpSecurity.headers().cacheControl();
    
            // 添加JWT filter
            httpSecurity
                    .apply(new TokenConfigurer(jwtTokenUtils));
    
        }
    
        public class TokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
            private final JwtTokenUtils jwtTokenUtils;
    
            public TokenConfigurer(JwtTokenUtils jwtTokenUtils){
    
                this.jwtTokenUtils = jwtTokenUtils;
            }
    
            @Override
            public void configure(HttpSecurity http) {
                JwtAuthenticationTokenFilter customFilter = new JwtAuthenticationTokenFilter(jwtTokenUtils);
                http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
            }
        }
    
    }
    
    

    7. 编写Controller进行测试

    登录逻辑:传递user与password参数,返回token

    @Slf4j
    @RestController
    @RequestMapping("/api/auth")
    @Api(tags = "系统授权接口")
    public class AuthController {
    
        private final JwtTokenUtils jwtTokenUtils;
    
        public AuthController(JwtTokenUtils jwtTokenUtils) {
            this.jwtTokenUtils = jwtTokenUtils;
        }
    
        @ApiOperation("登录授权")
        @GetMapping(value = "/login")
        public String login(String user,String password){
            Map map = new HashMap();
            map.put("user",user);
            map.put("password",password);
            return jwtTokenUtils.createToken(map);
        }  
    }
    

    使用IDEA Rest Client测试如下:Request请求Response返回token
    验证逻辑:传递token,验证成功后返回用户信息
    Request请求,Headers加入Authorization:(token值)Response结果
    token验证错误返回401:
    在这里插入图片描述

  • 相关阅读:
    HDU 4325 Flowers(树状数组)
    HDU 1166 敌兵布阵(树状数组)
    linux网络编程之一-----多播(组播)编程
    对 /dev/shm 认识
    使用GDB调试STL容器
    Android中图片优化之webp使用
    Android后台进程与前台线程间的区别使用
    Android如何从外部跳进App
    熟悉Android开发不得不知道的技巧
    Java代码规范文档
  • 原文地址:https://www.cnblogs.com/zhuhuix/p/12974415.html
Copyright © 2020-2023  润新知