> 原文阅读:https://blog.csdn.net/jpgzhu/article/details/105200598 > - 本文内容已应用于dmp框架,并对com.keyou.dmp.util.JwtTokenUtils#getAuthentication进行了优化,增加了权限信息的存放,最新代码请查看dmp; > - 本框架是在SpringSecurity的基础上引入了jwt,并没有引入oauth2。当然SpringSecurity在引入OAuth2的前提下,可以使用jwt作为token存储方案,如ocp就是这样使用的。 #### 前言 > 微服务架构,前后端分离目前已成为互联网项目开发的业界标准,其核心思想就是前端(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工程项目目录](https://img-blog.csdnimg.cn/20200330155505528.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2pwZ3podQ==,size_16,color_FFFFFF,t_70) #### 2、导入SpringSecurity与JWT的相关依赖 pom文件加入以下依赖 ```java <!--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配置信息: ```java #jwt jwt: header: Authorization # 令牌前缀 token-start-with: Bearer # 使用Base64对该令牌进行编码 base64-secret: XXXXXXXXXXXXXXXX(制定您的密钥) # 令牌过期时间 此处单位/毫秒 token-validity-in-seconds: 14400000 ``` 创建一个jwt的配置类,并注入Spring,便于程序中灵活调用 ```java @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 + " "; } } ``` 定义无权限访问类 ```java @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()); } } ``` 定义认证失败处理类 ```java @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功能 ```java @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校验 ```java @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启动类中,加入以下代码: ```java @Bean public FilterRegistrationBean registration(JwtAuthenticationTokenFilter filter) { FilterRegistrationBean registration = new FilterRegistrationBean(filter); registration.setEnabled(false); return registration; } 123456 ``` #### 6. SpringSecurity的关键配置 SpringBoot推荐使用配置类来代替xml配置,该类中涉及了以上几个bean来供security使用 - JwtAccessDeniedHandler :无权限访问 - jwtAuthenticationEntryPoint :认证失败处理 - jwtAuthenticationTokenFilter :token验证的过滤器 ```java 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 ```java @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请求](https://img-blog.csdnimg.cn/20200330190123833.png)![Response返回token](https://img-blog.csdnimg.cn/20200330190153673.png) 验证逻辑:传递token,验证成功后返回用户信息 ![Request请求,Headers加入Authorization:(token值)](https://img-blog.csdnimg.cn/20200330193526960.png)![Response结果](https://img-blog.csdnimg.cn/20200330193639420.png) token验证错误返回401: ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200330201138809.png) >其他资料: [SpringBoot整合SpringSecurity实现JWT认证](https://blog.csdn.net/jpgzhu/article/details/105200598) [Spring Security登录认证【前后端分离】](https://blog.csdn.net/godleaf/article/details/108318403) [Springboot+SpringSecurity_jwt整合原理](https://www.processon.com/view/5eee114a07912929cb51e062?fromnew=1#map)