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.passwordoverride 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 falseval id = jwtConfig.decodeUserId(jwt) ?: return trueval user = userService.findByUid(id) ?: return trueval decodedJWT = jwtConfig.verifyLoginJwt(jwt, user) ?: return true// 主要邏輯:驗證成功,在上下文中設置 AuthenticationTokensetAuthenticationToken(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@applyval cookie = Cookie(jwtConfig.cookieName, data?.token)cookie.isHttpOnly = truecookie.secure = falsecookie.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.cookiesif (request is HttpServletRequest) return request.cookiesif (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 接口
){@Beanfun filterChain(http: HttpSecurity): SecurityFilterChain = http.run {// 由于使用 JWT 所以這里 關閉 sessionsessionsessionManagement { 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() }// 生產下必須關閉 csrfcsrf { 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 */@Beanfun authenticationProvider(passwordEncoder: PasswordEncoder): AuthenticationProvider =object : AuthenticationProvider {override fun authenticate(authentication: Authentication): Authentication {val identifier = authentication.nameval 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)}}/*** 基于用戶名和密碼或使用用戶名和密碼進行身份驗證*/@Beanfun 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)}