本文将会带你使用springboot3,spring security6 jwt实现基于jwt Token 的身份认证。
maven依赖版本
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 其他无关内容省略-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-json</artifactId>
</dependency>
</dependencies>
实现思路
这里我们参考官网的[示例项目](spring-security-samples/servlet/spring-boot/java/jwt/login at main · spring-projects/spring-security-samples (github.com))。但是这个示例是基于http Basic的授权登录,这里我们需要基于账号密码的json数据格式的授权登录。基本思路是实现认证授权过滤器AbstractAuthenticationProcessingFilter
读取requestbody中的账号密码,成功之后生成jwt token返回给客户端,这一步需实现AuthenticationSuccessHandler
核心配置
java
package com.breeze.breezeAdmin.securityConfig;
// 省略import
@Configuration
public class SecurityConfiguration {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private AuthorizationManager<RequestAuthorizationContext> authorizationManager;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, AuthenticationConfiguration authenticationConfiguration) throws Exception {
JsonLoginFilter jsonLoginFilter = new JsonLoginFilter();
jsonLoginFilter.setAuthenticationManager(authenticationConfiguration.getAuthenticationManager());
jsonLoginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
jsonLoginFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/swagger-ui.html", "/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest()
.authenticated()
)
.csrf(AbstractHttpConfigurer::disable)
//jwt配置
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()))
.sessionManagement(AbstractHttpConfigurer::disable)
.addFilterBefore(jsonLoginFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
)
;
return http.build();
}
@Bean
UserDetailsService users() {
// @formatter:off
return new InMemoryUserDetailsManager(
User.withUsername("user")
.password("{noop}password")
.authorities("app")
.build()
);
// @formatter:on
}
}
jwt加密和解密
这里我们使用到了spring-boot-starter-oauth2-resource-server提供的jwt能力。
java
package com.breeze.breezeAdmin.securityConfig;
// 省略import
@Configuration
public class JwtConfig {
@Value("${jwt.public.key}")
RSAPublicKey key;
@Value("${jwt.private.key}")
RSAPrivateKey priv;
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
}
@Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
}
配置公钥私钥
yml
jwt:
private.key: classpath:app.key
public.key: classpath:app.pub
app.key,app.pub放置在classpath下,和application.yml同级
认证过滤器
实现认证过滤器我们可以参照UsernamePasswordAuthenticationFilter这个spring security内置的表单登录认证过滤器,不同在于接受的HTTP request body格式不同。
java
package com.breeze.breezeAdmin.securityConfig;
// 省略import
public class JsonLoginFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST");
@Getter
@Setter
private String usernameParameter = "username";
@Getter
@Setter
private String passwordParameter = "password";
public JsonLoginFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String contentType = request.getContentType();
if(!contentType.equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) && !contentType.equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)){
throw new AuthenticationServiceException("Authentication contentType not supported: " + request.getMethod());
}
if(request.getContentLength() <= 0){
throw new AuthenticationServiceException("Authentication contentLength not supported: " + request.getMethod());
}
try {
Map<String,String> map = objectMapper.readValue(request.getInputStream(),Map.class);
String username = map.getOrDefault(usernameParameter, "").trim();
String password = map.getOrDefault(passwordParameter, "").trim();
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
认证后置处理 生成jwt token
java
package com.breeze.breezeAdmin.securityConfig;
// 省略import
@Slf4j
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Autowired
private JwtEncoder encoder;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
Instant now = Instant.now();
long expiry = 60*60*24L;
String scope = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plusSeconds(expiry))
.subject(authentication.getName())
.claim("scope", scope)
.build();
var jwt = this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
SingleResponse<String> responseBody = SingleResponse.of(jwt);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write(JSONUtil.toJsonStr(responseBody));
log.info("登录成功");
}
}
失败处理
java
package com.breeze.breezeAdmin.securityConfig;
// 省略import
@Slf4j
@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Response responseBody = SingleResponse.buildFailure("-1","用户名或密码错误");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(responseBody));
log.info("用户名或密码错误");
}
}
最后
遗留一个小问题,jwt token续期。