Spring Security jwt 身份验证

Posted

技术标签:

【中文标题】Spring Security jwt 身份验证【英文标题】:Spring Security jwt authentication 【发布时间】:2022-01-15 21:27:31 【问题描述】:

我正在使用 spring boot 和 jwt 创建一个后端应用程序以进行身份​​验证。

我的问题是我无法让当局按照我希望他们的方式工作。我可以登录我的用户并取回 jwt。但是,当在服务器上请求仅允许特定权限的路径时,即使我没有发送授权标头,我也会返回 200。

这是我的代码:

SecurityConfig.kt

@EnableWebSecurity
class SecurityConfig(
    private val userDetailsService: UserDetailsService,
    private val jwtAuthenticationFilter: JwtAuthenticationFilter)
    : WebSecurityConfigurerAdapter() 

    override fun configure(httpSecurity: HttpSecurity) 
        httpSecurity.csrf().disable()
            .authorizeRequests()
            .antMatchers( "/$Constants.API_PATH/$Constants.USER_PATH/**") // translates to /api/v1/users/**
            .hasAuthority("USER")
            .antMatchers("/$Constants.API_PATH/$Constants.EMAIL_VERIFICATION_PATH/**")
            .permitAll()
            .antMatchers("/$Constants.API_PATH/$Constants.LOGIN_PATH")
            .permitAll()
            .anyRequest()
            .authenticated()
        httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
    

    override fun configure(auth: AuthenticationManagerBuilder) 
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(encoder())
    

    @Bean
    fun encoder(): PasswordEncoder 
        return BCryptPasswordEncoder() // salts the password
    

    @Bean(BeanIds.AUTHENTICATION_MANAGER)
    override fun authenticationManagerBean(): AuthenticationManager 
        return super.authenticationManagerBean()
    

LoginService.kt(不知道这段代码是否相关,但也许这里有问题)

@Service
class LoginService(private val authenticationManager: AuthenticationManager,
                   private val jwtProvider: JwtProvider,
                   private val userService: UserService) 

    fun login(loginRequest: LoginRequest): LoginResponse 
        // authenticate internally call UserDetailsService
        val authenticate = authenticationManager.authenticate(
            UsernamePasswordAuthenticationToken(loginRequest.email, loginRequest.password))
        SecurityContextHolder.getContext().authentication = authenticate
        //TODO implement logic for employer

        val jwtToken = jwtProvider.generateToken(authenticate)
        val jobseeker = jobseekerService.getJobseeker(loginRequest.email)

        return LoginResponse(jwtToken, jobseeker)
    

UserDetailsS​​erviceImpl.kt

@Service
class UserDetailsServiceImpl(private val userRepository: UserRepository) : UserDetailsService 

    /* we don't use usernames, so we pass the email address here*/
    override fun loadUserByUsername(username: String?): UserDetails 
        val user = userRepository.findByEmail(username) ?: throw CustomException("jobseeker not found")


        return org.springframework.security.core.userdetails.User(user.email, user.getHashedSecret(),
            jobseeker.getActivated(), true, true, true,
            getAuthorities("USER"))
    

    private fun getAuthorities(authority: String) = singletonList(SimpleGrantedAuthority(authority))

JwtProvider.kt(我知道“秘密”不是一个好秘密,它只是为了测试目的)

@Service
class JwtProvider 
    val secret: String = "secret"

    // TODO: IMPORTANT -> the keystore is selfsigned and needs to be changed as soon as we get
    //  to the first production version
    private lateinit var keyStore: KeyStore

    @PostConstruct
    fun init() 
        try 
            keyStore = KeyStore.getInstance("JKS")
            // very helpful for resources: https://***.com/questions/4301329/java-class-getresource-returns-null
            val resourceAsStream: InputStream? = javaClass.getResourceAsStream("/key.jks")
            keyStore.load(resourceAsStream, secret.toCharArray())
         catch (ex: Exception) 
            throw CustomException("problem while loading keystore")
        
    

    fun generateToken(authentication: Authentication): String 
        val pricipal: User = authentication.principal as User
        return Jwts.builder()
            .setSubject(pricipal.username)
            .signWith(getPrivateKey())
            .compact()
    

    private fun getPrivateKey(): PrivateKey 
        return try 
            keyStore.getKey("key", secret.toCharArray()) as PrivateKey
         catch (ex: Exception) 
            logger.error(ex.message)
            throw CustomException("problem while retreiving the private key for jwt signing")
        
    

    private fun getPublicKey(): PublicKey 
        return try 
            keyStore.getCertificate("key").publicKey
         catch (ex: KeyStoreException) 
            throw CustomException("problem while retreiving public key")
        
    

    fun validateToken(jwt: String): Boolean 
        // using parseClaimsJws() because the jwt is signed
        Jwts.parserBuilder().setSigningKey(getPublicKey()).build().parseClaimsJws(jwt)
        return true
    

    fun getEmailFromJwt(jwt: String): String 
        return Jwts.parserBuilder().setSigningKey(getPublicKey()).build().parseClaimsJws(jwt).body.subject
    

JwtAuthenticationFilter.kt

@Component
class JwtAuthenticationFilter(
    private val jwtProvider: JwtProvider,
    private val userDetailsService: UserDetailsService
) : OncePerRequestFilter() 

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) 
        val jwt: String = getJwtFromRequest(request)

        if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) 
            val email: String = jwtProvider.getEmailFromJwt(jwt)
            val userDetails: UserDetails = userDetailsService.loadUserByUsername(email)
            val authentication = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
            authentication.details = (WebAuthenticationDetailsSource().buildDetails(request))

            SecurityContextHolder.getContext().authentication = authentication
        

        filterChain.doFilter(request, response)
    

    fun getJwtFromRequest(request: HttpServletRequest): String 
        val bearerToken: String = request.getHeader("Authorization") ?: ""
        // hasText() is needed because there are api without auth and this string could be null
        if(StringUtils.hasText(bearerToken))
            return bearerToken.substringAfter(" ")

        return bearerToken
    

所以,当我现在尝试不使用 jwt 作为不记名令牌的 GET http://localhost:8080/api/v1/usersGET http://localhost:8080/api/v1/users/<uuid> 时,我得到的是 200 而不是 403。

我已经尝试了好几个小时,但完全被卡住了。

我对 kotlin 和 spring boot 也很陌生,如果有任何关于以更优雅的方式编写我提供的代码的提示,我将不胜感激。

【问题讨论】:

【参考方案1】:

如果jwt 在您的JwtAuthenticationFilter 中为空,您可以简单地抛出异常:

...
val jwt: String = getJwtFromRequest(request)
if (jwt.isEmpty()) 
 throw new AccessDeniedException("Missing JWT Token")

...

或者,您可以执行以下操作:

...
val jwt: String = getJwtFromRequest(request)
if (jwt.isEmpty()) 
 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);  
 return;

...

【讨论】:

感谢您的回答,但这不是我想要实现的行为。我希望未经身份验证的用户可以访问登录和 email_verification API,并且只有用户 API 需要身份验证。我认为antMatchers() 结合SecurityConfig.kt 中的hasAuthority() 应该可以解决问题,但事实并非如此。你知道为什么吗?

以上是关于Spring Security jwt 身份验证的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Spring Security 中为所有请求添加 jwt 身份验证标头?

如何在 Spring Security 中实现基于 JWT 的身份验证和授权

在 Spring Security UsernamePasswordAuthenticationFilter JWT 身份验证中设置自定义登录 url

我可以有两个 Spring Security 配置类:一个使用基本身份验证保护一些 API,另一个使用 JWT 令牌保护 API?

Spring Security JWT - 通过授权标头的邮递员身份验证有效,但从我的 Angular 前端不起作用

前后端分离的Web应用程序中使用Spring Security+Mybatis+JWT非对称加密+动态权限管理:身份验证过滤器