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)
}