小Hub领读:最近一个安全项目,参考了这个例子,还可以做
作者:智慧zhuhuix https://blog.csdn.net/jpgzhu/article/details/105200598
前言微架构,之前调用和应用程序的直接应用程序,即应用程序启动和应用程序,作为互联网应用程序的应用程序,通过其应用程序的API接口(API)返回5个应用程序的API接口(API),应用程序和应用程序。 微架构下,传统的认证方式适用于应用运行机制,而传统的认证方式适用于应用运行机制,而传统的认证方式适用于应用运行机制站点的单点登录(SSO)场景 目录该文会通过创建 SpringBoot 项目集成 SpringSecurity,实现完整的 JWT 认证机制,主要步骤如下: 创建 SpringBoot 工程 导入 SpringSecurity 与 JWT 的相关依赖 定义 SpringSecurity 需要的基础处理类 制造JWT token 工具类 实现令牌 验证的过滤器 SpringSecurity的关键配置 编写 Controller 进行测试
1、创建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代币工具类工具类实现创建令牌与校验令牌功能 @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. implementation token 验证的过滤器该类继承 OncePerRequestFilter,它能够确保在请求中通过一次过滤 该类使用 JwtToken 只顾工具类进行令牌校验 @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官方让重复执行的过滤器实现一次过程执行的解决方案,见官网地址:https://docs./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来供安全使用 JwtAccessDeniedHandler :无权限访问 jwtAuthenticationEntryPoint :认证失败处理 jwtAuthenticationTokenFilter :令牌验证的过滤器
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进行测试登录逻辑:提交用户和密码参数,返回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 测试如下: 验证逻辑:传递token,验证成功后返回用户信息
token 验证错误返回401:
(完)
|