SpringBoot整合SpringSecurity实现JWT认证的项目实践

作者:智慧zhuhuix 时间:2022-06-23 22:09:09 

前言

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

1、创建SpringBoot工程

SpringBoot整合SpringSecurity实现JWT认证的项目实践

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测试如下:

SpringBoot整合SpringSecurity实现JWT认证的项目实践

SpringBoot整合SpringSecurity实现JWT认证的项目实践

验证逻辑:传递token,验证成功后返回用户信息

SpringBoot整合SpringSecurity实现JWT认证的项目实践

SpringBoot整合SpringSecurity实现JWT认证的项目实践

token验证错误返回401:

SpringBoot整合SpringSecurity实现JWT认证的项目实践

来源:https://zhuhuix.blog.csdn.net/article/details/105200598

标签:SpringBoot,SpringSecurity,JWT,认证
0
投稿

猜你喜欢

  • springmvc 分页查询的简单实现示例代码

    2022-01-09 11:08:22
  • Netty分布式pipeline管道创建方法跟踪解析

    2023-11-03 02:55:51
  • Android的消息机制

    2023-08-05 10:19:28
  • 微服务通过Feign调用进行密码安全认证操作

    2023-07-30 02:43:38
  • Java使用ThreadLocal实现当前登录信息的存取功能

    2023-06-06 12:00:34
  • Mybatis初始化知识小结

    2023-11-01 13:59:27
  • Java数据结构与算法入门实例详解

    2023-11-28 21:44:06
  • MyBatis-Plus实现多数据源的示例代码

    2023-11-11 12:58:11
  • IDEA快速搭建spring boot项目教程(Spring initializr)

    2023-08-17 21:11:16
  • 基于javaMybatis存进时间戳的问题

    2023-11-29 02:55:51
  • Java实现文件和base64流的相互转换功能示例

    2023-11-18 07:45:01
  • JavaWeb开发之使用jQuery与Ajax实现动态联级菜单效果

    2023-11-28 19:46:08
  • Spring Security之默认的过滤器链及自定义Filter操作

    2023-11-24 02:48:35
  • Spring JPA 增加字段执行异常问题及解决

    2023-06-25 23:55:58
  • Java 设计模式中的策略模式详情

    2023-08-06 03:45:11
  • Java面试题冲刺第二十四天--并发编程

    2023-08-31 05:39:02
  • IDEA解决springboot热部署失效问题(推荐)

    2023-08-12 10:40:49
  • SpringBoot详细讲解静态资源导入的实现

    2023-07-26 13:23:21
  • 你知道在Java中Integer和int的这些区别吗?

    2023-09-04 02:50:18
  • java的泛型你真的了解吗

    2022-07-25 09:40:06
  • asp之家 软件编程 m.aspxhome.com