kotlin + spirngboot3 + spring security6 配置登录与JWT

发布于:2025-04-18 ⋅ 阅读:(24) ⋅ 点赞:(0)

1. 导包

implementation("com.auth0:java-jwt:3.14.0")
implementation("org.springframework.boot:spring-boot-starter-security")

配置用户实体类


@Entity
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val uid: Long = 0,

    @Column(nullable = false, name = "username")
    val username: String = String.EMPTY_STRING,

    @Column(nullable = false, name = "password")
    val password: String = String.EMPTY_STRING,

    @Column(nullable = true)
    val email: String? = null,

    @Column(nullable = true)
    val phone: String? = null,
) {

	// 该方法作用是将实体类转为 UserUserDetails ,下文需要用到
    fun convertUserUserDetails(): UserDetailsImpl = UserDetailsImpl(this)

    class UserDetailsImpl(val user: User) : UserDetails {
        override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
            return mutableListOf(SimpleGrantedAuthority("ROLE_USER"))
        }

        override fun getPassword(): String = user.password
        override fun getUsername(): String = user.username
    }
}

配置用户服务

@Service
class UserService(
    override val repository: UserRepository,
) : UserDetailsService {
    // ..... 其他方法

	// 需要继承 UserDetailsService  并重写该方法,用于查找用户
	// 通常 identifier 传入的 username,但是我的代码并非使用 username 登录,这里稍微替换下即可
	// 如果没有查找到 user 抛出异常
	// 方法返回需要 UserDetails 需要将 user 实体类转为 UserDetails 
    override fun loadUserByUsername(identifier: String): UserDetailsImpl = when {
        identifier.isEmail -> findByEmail(identifier) ?: throw UsernameNotFoundException("Email not registered")
        identifier.isNumber -> findByPhone(identifier) ?: throw UsernameNotFoundException("Phone not registered")
        else -> throw UsernameNotFoundException("not found account $identifier")
    }.convertUserUserDetails()
}

配置 JWT 过滤器

// 主要方法为 isPass
// jwtConfig 是自己写的用于验证和签发 jwt,并存储 jwt 的设置,可以自己自行替换
class JwtTokenFilter(private val userService: UserService, private val jwtConfig: JwtConfig) : OncePerRequestFilter() {

    private fun isPass(request: ServletRequest, response: HttpServletResponse): Boolean {
        val jwt = getJwtString(request) ?: return false
        val id = jwtConfig.decodeUserId(jwt) ?: return true
        val user = userService.findByUid(id) ?: return true
        val decodedJWT = jwtConfig.verifyLoginJwt(jwt, user) ?: return true
        // 主要逻辑:验证成功,在上下文中设置 AuthenticationToken
        setAuthenticationToken(user)
        if (jwtConfig.autoRefresh) refresh(response, user, decodedJWT)
        return false
    }

    private fun refresh(
        response: ServletResponse,
        user: User,
        decodedJWT: DecodedJWT,
    ): ResultLoginBean {
        if (decodedJWT.expiresAt.time - System.currentTimeMillis() <= 60 * 1000) {
            return ResultLoginBean.success(user, jwtConfig).apply {
                if (!jwtConfig.autoSetCookie) return@apply
                val cookie = Cookie(jwtConfig.cookieName, data?.token)
                cookie.isHttpOnly = true
                cookie.secure = false
                cookie.maxAge = (jwtConfig.effectiveTime).toInt()
                cookie.path = "/"
                (response as HttpServletResponse).addCookie(cookie)
            }
        }
        return ResultLoginBean.success(ResultLoginBean.DataBean(decodedJWT.token))
    }

    private fun getJwtString(request: ServletRequest): String? {
        val value = getCookie(request)?.firstOrNull() { it.name == jwtConfig.cookieName }?.value ?: ""
        val value2 = getHeaders(request, "Authorization")?.replace(jwtConfig.headerPrefix, "") ?: ""
        return if (jwtConfig.fromCookie && value.isNotBlank()) {
            value
        } else if (jwtConfig.fromHeader && value2.isNotBlank()) {
            value2
        } else {
            null
        }
    }

    private fun getCookie(request: ServletRequest): Array<out Cookie>? {
        if (request is RequestFacade) return request.cookies
        if (request is HttpServletRequest) return request.cookies
        if (request is ServletRequestWrapper) {
            return getCookie(request.request)
        }
        return null
    }

    private fun getHeaders(request: ServletRequest, name: String): String? {
        if (request is RequestFacade) return request.getHeader(name)
        if (request is HttpServletRequest) return request.getHeader(name)
        if (request is ServletRequestWrapper) {
            return getHeaders(request.request, name)
        }
        return null
    }

    private fun setAuthenticationToken(user: User) {
        SecurityContextHolder.getContext().authentication = usernamePasswordAuthenticationToken(user)
    }

	// 这个类很长有用的不多,注意以下几点
	// 1. 认证成功,则在上下文中设置 usernamePasswordAuthenticationToken
	// 2. 认证失败,清空上下文
	// 3. 无论成功与失败都调用下一个过滤器,当然如果失败了你不调用下一个过滤器也行
    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain,
    ) {
    	// 如果前端没有传入 token 则清理上下文
        if (isPass(request, response)) {
            SecurityContextHolder.clearContext()
        }
        // 无论Token 是否验证成功都传给下一个过滤器
        filterChain.doFilter(request, response)
    }

}

配置 Spring Security

spring security 6 需要使用 filterChain 来配置认证链,并且 推荐使用 DSL 方式进行配置即Lambda方式

@Configuration
@EnableWebSecurity
class SpringSecurityConfig(
	private val userService: UserService, // 你的 User 服务用于查询用户的,但是需要实现 UserDetailsService 接口
){
    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain = http.run {
    	// 由于使用 JWT 所以这里 关闭 sessionsession
        sessionManagement { it.sessionCreationPolicy(SessionCreationPolicy.STATELESS) }
        // 配置哪些请求需要验证或者放行
        authorizeHttpRequests {
            it.requestMatchers("/api/authority/**").permitAll()
            // 这种模糊匹配的范围很大需要在精确的路径之后配置,否则精确配置不生效
            it.requestMatchers("/api/**").authenticated() 
            it.anyRequest().permitAll()
        }
        // 配置异常处理
        exceptionHandling {
        	// 没有登录的错误
            it.authenticationEntryPoint { _, response, _ ->
                response.close(BaseBean(BaseBean.Code.LOGIN_EXPIRED))
            }
            // 没有权限的错误
            it.accessDeniedHandler { _, response, _ ->
                response.close(BaseBean(BaseBean.Code.PERMISSION_DENIED))
            }
        }
        // 添加 JWT 的认证过滤器到 UsernamePasswordAuthenticationFilter 链中
        addFilterBefore(JwtTokenFilter(userService, jwtConfig), UsernamePasswordAuthenticationFilter::class.java)
        isDisableFormLogin(false)
        httpBasic { it.disable() }
        rememberMe { it.disable() }
        // 生产下必须关闭 csrf
        csrf { it.disable() }
        cors(Customizer.withDefaults())
        build()
    }
}


    /**
     * 两种登录方式,一种使用自定义登录接口,,一种使用框架本身的表单登录
     * 使用框架本身的表单登录会覆盖  /api/authority/login Controller
     * /api/authority/login Controller 实现不展出了,逻辑是验证用户名和密码之后返回 toekn(jwt)
     */
    private fun HttpSecurity.isDisableFormLogin(isDisable: Boolean) {
        if (isDisable) {
            formLogin { it.disable() }
            return
        }
        formLogin {
            it.loginProcessingUrl("/api/authority/login") // 指定你的登录接口
            it.usernameParameter("identifier")
            it.passwordParameter("password")
            it.successHandler { request, response, auth ->
            	// 这里将登录后 Token 发生回了前端
            	// auth.principal as User.UserDetailsImpl 能转为 UserDetailsImpl  的愿意为,下面验证的时候传入的 UserDetailsImpl 
                val user = (auth.principal as User.UserDetailsImpl).getUser()
                response.close(ResultLoginBean.success(user, jwtConfig, response))
            }
            it.failureHandler { request, response, exception ->
                response.close(BaseBean.fail("登录失败: ${exception.message}"))
            }
        }
    }
        
    /**
     * 身份校验机制、身份验证提供程序(isDisableFormLogin 中设置为 false)
     * 验证成功后会在认证链里面传递 UsernamePasswordAuthenticationToken 
     */
    @Bean
    fun authenticationProvider(passwordEncoder: PasswordEncoder): AuthenticationProvider =
        object : AuthenticationProvider {
            override fun authenticate(authentication: Authentication): Authentication {
                val identifier = authentication.name
                val password = authentication.credentials.toString()
                val user = userService.loadUserByUsername(identifier)
                if (passwordEncoder.matches(password, user.password)) {
                    return usernamePasswordAuthenticationToken(user)
                } else {
                    throw BadCredentialsException("The password is wrong")
                }
            }

            override fun supports(authentication: Class<*>): Boolean {
                return UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
            }
        }

    /**
     * 基于用户名和密码或使用用户名和密码进行身份验证
     */
    @Bean
    fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
        config.getAuthenticationManager()

	// 配置拓展方法
	fun <T : Any> ServletResponse.close(data: BaseBean<T>) {
	    this.contentType = "application/json;charset=UTF-8"
	    this.writer.write(data.toString())
	    this.writer.flush()
	    this.writer.close()
	}
	
	@Bean(name = ["passwordEncoder", "bcryptPasswordEncoder"])
	fun passwordEncoder(): PasswordEncoder {
	    return BCryptPasswordEncoder()
	}
	fun usernamePasswordAuthenticationToken(user: User): UsernamePasswordAuthenticationToken {
	    return usernamePasswordAuthenticationToken(user.convertUserUserDetails())
	}
	
	fun usernamePasswordAuthenticationToken(user: User.UserDetailsImpl): UsernamePasswordAuthenticationToken {
	    return UsernamePasswordAuthenticationToken(user, user.password,user.authorities)
	}

网站公告

今日签到

点亮在社区的每一天
去签到