使用spring security6 jwt 实现基于 Token 的身份认证

本文将会带你使用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续期。

相关推荐
xiaosannihaiyl2423 分钟前
Scala语言的函数实现
开发语言·后端·golang
山山而川粤6 小时前
母婴用品系统|Java|SSM|JSP|
java·开发语言·后端·学习·mysql
玉红7778 小时前
R语言的数据类型
开发语言·后端·golang
lvbu_2024war0110 小时前
MATLAB语言的网络编程
开发语言·后端·golang
问道飞鱼10 小时前
【Springboot知识】Springboot进阶-实现CAS完整流程
java·spring boot·后端·cas
Q_192849990610 小时前
基于Spring Boot的电影网站系统
java·spring boot·后端
豌豆花下猫10 小时前
Python 潮流周刊#83:uv 的使用技巧(摘要)
后端·python·ai
凡人的AI工具箱10 小时前
每天40分玩转Django:Django部署概述
开发语言·数据库·后端·python·django
SomeB1oody11 小时前
【Rust自学】7.2. 路径(Path)Pt.1:相对路径、绝对路径与pub关键字
开发语言·后端·rust
SomeB1oody11 小时前
【Rust自学】7.3. 路径(Path)Pt.2:访问父级模块、pub关键字在结构体和枚举类型上的使用
开发语言·后端·rust