参考文献

基于SpringBoot2.2.2.RELEASE

依赖

1
2
3
4
// springboot_version= '2.2.2.RELEASE'
implementation "org.springframework.boot:spring-boot-starter:$springboot_version"
implementation "org.springframework.boot:spring-boot-starter-web:$springboot_version"
implementation "org.springframework.boot:spring-boot-starter-security:$springboot_version"

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class WebSecurityConfig : WebSecurityConfigurerAdapter() {

@Autowired
private val userDetailsService: UserDetailsService? = null

@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
// 使用自定义登录身份认证组件
auth.authenticationProvider(JwtAuthenticationProvider(userDetailsService))

}

override fun configure(web: WebSecurity?) {
super.configure(web)
}

override fun configure(http: HttpSecurity) {
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/user/login").permitAll()
.antMatchers("/user/create-user").permitAll()
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(HttpStatusReturningLogoutSuccessHandler())
// 开启登录认证过滤器
http.addFilterBefore(JwtLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter::class.java)
// 访问控制登录状态检查器
http.addFilterBefore(
JwtAuthenticationFilter(authenticationManager()),
UsernamePasswordAuthenticationFilter::class.java
)

}

@Bean
override fun authenticationManager(): AuthenticationManager {
return super.authenticationManager()
}

@Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder();
}
}

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.holelin.security

import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.holelin.util.HttpUtils
import com.holelin.util.JwtTokenUtils
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.nio.charset.Charset
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse


class JwtLoginFilter(authManager: AuthenticationManager?) : UsernamePasswordAuthenticationFilter() {
@Throws(IOException::class, ServletException::class)
override fun doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain) {
// POST 请求 /login 登录时拦截, 由此方法触发执行登录认证流程,可以在此覆写整个登录认证逻辑
super.doFilter(req, res, chain)
}

//这个方法是用来去尝试验证用户的
@Throws(AuthenticationException::class)
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
// 可以在此覆写尝试进行登录认证的逻辑,登录成功之后等操作不再此方法内
// 如果使用此过滤器来触发登录认证流程,注意登录请求数据格式的问题
// 此过滤器的用户名密码默认从request.getParameter()获取,但是这种
// 读取方式不能读取到如 application/json 等 post 请求数据,需要把
// 用户名密码的读取逻辑修改为到流中读取request.getInputStream()
val body = getBody(request)
val jsonObject: JSONObject = JSON.parseObject(body)
var username: String = jsonObject.getString("username")
var password: String = jsonObject.getString("password")
username = username.trim { it <= ' ' }
val authRequest = JwtAuthenticationToken(username, password)

// Allow subclasses to set the "details" property
setDetails(request, authRequest)
return authenticationManager.authenticate(authRequest)
}

//成功之后执行的方法
@Throws(IOException::class, ServletException::class)
override fun successfulAuthentication(
request: HttpServletRequest?, response: HttpServletResponse, chain: FilterChain?,
authResult: Authentication?
) {
// 存储登录认证信息到上下文
SecurityContextHolder.getContext().authentication = authResult
// 记住我服务
rememberMeServices.loginSuccess(request, response, authResult)
// 触发事件监听器
if (eventPublisher != null) {
eventPublisher.publishEvent(InteractiveAuthenticationSuccessEvent(authResult, this.javaClass))
}
// 生成并返回token给客户端,后续访问携带此token
val token = JwtAuthenticationToken(null, null, authResult?.let { JwtTokenUtils.generateToken(it) })

HttpUtils.write(response, token)
}

/**
* 获取请求Body
* @param request
* @return
*/
private fun getBody(request: HttpServletRequest): String {
val sb = StringBuilder()
var inputStream: InputStream? = null
var reader: BufferedReader? = null
try {
inputStream = request.inputStream
reader = BufferedReader(InputStreamReader(inputStream, Charset.forName("UTF-8")))
var line: String? = ""
while (reader.readLine().also { line = it } != null) {
sb.append(line)
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (inputStream != null) {
try {
inputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
if (reader != null) {
try {
reader.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
return sb.toString()
}

init {
authenticationManager = authManager
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.holelin.security

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder


class JwtAuthenticationProvider(userDetailsService: UserDetailsService?) :
DaoAuthenticationProvider() {
@Throws(AuthenticationException::class)
override fun authenticate(authentication: Authentication?): Authentication {
// 可以在此处覆写整个登录认证逻辑
return super.authenticate(authentication)
}

@Throws(AuthenticationException::class)
override fun additionalAuthenticationChecks(
userDetails: UserDetails,
authentication: UsernamePasswordAuthenticationToken
) {
// 可以在此处覆写密码验证逻辑
super.additionalAuthenticationChecks(userDetails, authentication)
}

init {
setUserDetailsService(userDetailsService)
passwordEncoder = BCryptPasswordEncoder()
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.holelin.security

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User


class JwtUserDetails(
username: String?, password: String?, enabled: Boolean, accountNonExpired: Boolean,
credentialsNonExpired: Boolean, accountNonLocked: Boolean, authorities: Collection<GrantedAuthority?>?
) :
User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
constructor(username: String?, password: String?, authorities: Collection<GrantedAuthority?>?) : this(
username,
password,
true,
true,
true,
true,
authorities
) {
}

companion object {
private const val serialVersionUID = 1L
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.holelin.security

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.GrantedAuthority


/**
* 自定义令牌对象
* @author Louis
* @date Jun 29, 2019
*/
class JwtAuthenticationToken : UsernamePasswordAuthenticationToken {
var token: String? = null

constructor(principal: Any?, credentials: Any?) : super(principal, credentials) {}
constructor(principal: Any?, credentials: Any?, token: String?) : super(principal, credentials) {
this.token = token
}

constructor(
principal: Any?,
credentials: Any?,
authorities: Collection<GrantedAuthority?>?,
token: String?
) : super(principal, credentials, authorities) {
this.token = token
}

companion object {
const val serialVersionUID = 1L
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.holelin.security

import com.holelin.util.SecurityUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
import java.io.IOException
import javax.servlet.FilterChain
import javax.servlet.ServletException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse


class JwtAuthenticationFilter @Autowired constructor(authenticationManager: AuthenticationManager?) :
BasicAuthenticationFilter(authenticationManager) {
@Throws(IOException::class, ServletException::class)
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request)
chain.doFilter(request, response)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.holelin.security

import org.springframework.security.core.GrantedAuthority


class GrantedAuthorityImpl(private var authority: String) : GrantedAuthority {
fun setAuthority(authority: String) {
this.authority = authority
}

override fun getAuthority(): String {
return authority
}

companion object {
private const val serialVersionUID = 1L
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.holelin.security

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.User


class JwtUserDetails(
username: String?, password: String?, enabled: Boolean, accountNonExpired: Boolean,
credentialsNonExpired: Boolean, accountNonLocked: Boolean, authorities: Collection<GrantedAuthority?>?
) :
User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities) {
constructor(username: String?, password: String?, authorities: Collection<GrantedAuthority?>?) : this(
username,
password,
true,
true,
true,
true,
authorities
) {
}

companion object {
private const val serialVersionUID = 1L
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.holelin.util

import com.alibaba.fastjson.JSONObject
import com.holelin.vo.HttpResult
import org.springframework.web.context.request.RequestContextHolder
import org.springframework.web.context.request.ServletRequestAttributes
import java.io.IOException
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse


object HttpUtils {
/**
* 获取HttpServletRequest对象
* @return
*/
val httpServletRequest: HttpServletRequest
get() = (RequestContextHolder.getRequestAttributes() as ServletRequestAttributes).request

/**
* 输出信息到浏览器
* @param response
* @param message
* @throws IOException
*/
@Throws(IOException::class)
fun write(response: HttpServletResponse, data: Any?) {
response.contentType = "application/json; charset=utf-8"
val result: HttpResult = HttpResult.ok(data)
val json: String = JSONObject.toJSONString(result)
response.writer.print(json)
response.writer.flush()
response.writer.close()
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
package com.holelin.util

import com.holelin.security.GrantedAuthorityImpl
import com.holelin.security.JwtAuthenticationToken
import io.jsonwebtoken.Claims
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import org.springframework.security.core.Authentication
import org.springframework.security.core.GrantedAuthority
import java.io.Serializable
import java.util.*
import javax.servlet.http.HttpServletRequest
import kotlin.collections.ArrayList
import kotlin.collections.HashMap


object JwtTokenUtils : Serializable {
private const val serialVersionUID = 1L

/**
* 用户名称
*/
private const val USERNAME = Claims.SUBJECT

/**
* 创建时间
*/
private const val CREATED = "created"

/**
* 权限列表
*/
private const val AUTHORITIES = "authorities"

/**
* 密钥
*/
private const val SECRET = "abcdefgh"

/**
* 有效期12小时
*/
private const val EXPIRE_TIME = (12 * 60 * 60 * 1000).toLong()

/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
fun generateToken(authentication: Authentication): String {
val claims: MutableMap<String, Any> = HashMap(3)
claims[USERNAME] = SecurityUtils.getUsername(authentication)
claims[CREATED] = Date()
claims[AUTHORITIES] = authentication.authorities
return generateToken(claims)
}

/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private fun generateToken(claims: Map<String, Any>): String {
val expirationDate = Date(System.currentTimeMillis() + EXPIRE_TIME)
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET)
.compact()
}

/**
* 从令牌中获取用户名
*
* @param token 令牌
* @return 用户名
*/
private fun getUsernameFromToken(token: String): String? {
val username: String?
username = try {
val claims = getClaimsFromToken(token)
claims!!.subject
} catch (e: Exception) {
null
}
return username
}

/**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
fun getAuthenticationFromToken(request: HttpServletRequest): Authentication? {
var authentication: Authentication? = null
// 获取请求携带的令牌
val token = getToken(request)
if (token != null) {
// 请求令牌不能为空
if (SecurityUtils.authentication == null) {
// 上下文中Authentication为空
val claims = getClaimsFromToken(token) ?: return null
val username = claims.subject ?: return null
if (isTokenExpired(token)) {
return null
}
val authors = claims[AUTHORITIES]
val authorities: MutableList<GrantedAuthority> = ArrayList()
if (authors != null && authors is List<*>) {
for (`object` in authors) {
authorities.add(GrantedAuthorityImpl((`object` as Map<*, *>)["authority"] as String))
}
}
authentication = JwtAuthenticationToken(username, null, authorities, token)
} else {
if (validateToken(token, SecurityUtils.username)) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.authentication
}
}
}
return authentication
}

/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private fun getClaimsFromToken(token: String): Claims? {
val claims: Claims? = try {
Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).body
} catch (e: Exception) {
null
}
return claims
}

/**
* 验证令牌
* @param token
* @param username
* @return
*/
private fun validateToken(token: String, username: String): Boolean {
val userName = getUsernameFromToken(token)
return userName == username && !isTokenExpired(token)
}

/**
* 刷新令牌
* @param token
* @return
*/
fun refreshToken(token: String): String? {
var refreshedToken: String?
try {
val claims = getClaimsFromToken(token)
claims!![CREATED] = Date()
refreshedToken = generateToken(claims)
} catch (e: Exception) {
refreshedToken = null
}
return refreshedToken
}

/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
private fun isTokenExpired(token: String): Boolean {
return try {
val claims = getClaimsFromToken(token)
val expiration: Date = claims!!.expiration
expiration.before(Date())
} catch (e: Exception) {
false
}
}

/**
* 获取请求token
* @param request
* @return
*/
private fun getToken(request: HttpServletRequest): String? {
var token = request.getHeader("Authorization")
val tokenHead = "Bearer "
if (token == null) {
token = request.getHeader("token")
} else if (token.contains(tokenHead)) {
token = token.substring(tokenHead.length)
}
if ("" == token) {
token = null
}
return token
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package com.holelin.util

import com.holelin.security.JwtAuthenticationToken
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.core.Authentication
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import javax.servlet.http.HttpServletRequest


object SecurityUtils {
/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
fun login(
request: HttpServletRequest?,
username: String?,
password: String?,
authenticationManager: AuthenticationManager
): JwtAuthenticationToken {
val token = JwtAuthenticationToken(username, password)
token.details = WebAuthenticationDetailsSource().buildDetails(request)
// 执行登录认证过程
val authentication: Authentication = authenticationManager.authenticate(token)
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().authentication = authentication
// 生成令牌并返回给客户端
token.token = JwtTokenUtils.generateToken(authentication)
return token
}

/**
* 获取令牌进行认证
* @param request
*/
fun checkAuthentication(request: HttpServletRequest?) {
// 获取令牌并根据令牌获取登录认证信息
val authentication: Authentication? = JwtTokenUtils.getAuthenticationFromToken(request!!)
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().authentication = authentication
}

/**
* 获取当前用户名
* @return
*/
val username: String
get() {
var username = ""
val authentication: Authentication? = authentication
if (authentication != null) {
val principal: Any = authentication.principal
if (principal is UserDetails) {
username = principal.username
}
}
return username
}

/**
* 获取用户名
* @return
*/
fun getUsername(authentication: Authentication): String {
var username: String = ""
val principal: Any = authentication.principal
if (principal is UserDetails) {
username = principal.username
}
return username
}


/**
* 获取当前登录信息
* @return
*/
val authentication: Authentication?
get() = if (SecurityContextHolder.getContext() == null) {
null
} else SecurityContextHolder.getContext().authentication
}

基于SpringBoot 2.7.4

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- 版本2.7.4-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 版本0.12.5 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>

配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
package cn.holelin.athena.config;

import cn.holelin.athena.config.security.CustomizeAccessDeniedHandler;
import cn.holelin.athena.config.security.ResourceAuthExceptionEntryPoint;
import cn.holelin.athena.config.security.SecuritySecurityCheckService;
import cn.holelin.athena.filter.JwtAuthenticationTokenFilter;
import cn.holelin.athena.service.NotAuthenticationService;
import cn.holelin.athena.service.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfiguration {
private final UserDetailsServiceImpl userDetailsService;

private final NotAuthenticationService notAuthenticationService;

public SecurityConfiguration(UserDetailsServiceImpl userDetailsService, NotAuthenticationService notAuthenticationService) {
this.userDetailsService = userDetailsService;
this.notAuthenticationService = notAuthenticationService;
}


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
//支持跨域
http.cors().and()
//csrf关闭
.csrf().disable()
//不使用session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().authorizeRequests(rep -> rep.antMatchers(notAuthenticationService.getPermitAllUrls().toArray(new String[0]))
.permitAll().anyRequest().authenticated())
.exceptionHandling()
//异常认证
.authenticationEntryPoint(new ResourceAuthExceptionEntryPoint())
.accessDeniedHandler(new CustomizeAccessDeniedHandler())
.and()
//token过滤
.addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class)
.userDetailsService(userDetailsService);
return http.build();
}


@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 获取AuthenticationManager
*
* @param configuration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}


@Bean("ssc")
public SecuritySecurityCheckService permissionService() {
return new SecuritySecurityCheckService();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package cn.holelin.athena.service;

import cn.hutool.core.collection.CollUtil;
import com.google.common.collect.Sets;
import cn.holelin.athena.entity.SysRole;
import cn.holelin.athena.entity.SysUser;
import cn.holelin.athena.mapper.SysUserMapper;
import cn.holelin.athena.mapper.SysUserRoleMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {


private final SysUserMapper sysUserMapper;
private final SysUserRoleMapper sysUserRoleMapper;

public UserDetailsServiceImpl(SysUserMapper sysUserMapper, SysUserRoleMapper sysUserRoleMapper) {
this.sysUserMapper = sysUserMapper;
this.sysUserRoleMapper = sysUserRoleMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

final SysUser sysUser = sysUserMapper.queryByUsername(username);
if (Objects.isNull(sysUser)) {
throw new UsernameNotFoundException("账号不存在");
}
final List<SysRole> sysRoles = sysUserRoleMapper.queryByUsername(username);
Set<String> dbAuthsSet = Sets.newHashSet();
if (CollUtil.isNotEmpty(sysRoles)) {
dbAuthsSet = sysRoles.stream().map(SysRole::getFlag).collect(Collectors.toSet());
}
//配置角色
Collection<GrantedAuthority> auths = AuthorityUtils
.createAuthorityList(dbAuthsSet.toArray(new String[0]));

return new User(sysUser.getUsername(), new BCryptPasswordEncoder().encode(sysUser.getPassword()), auths);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package cn.holelin.athena.service;

import cn.holelin.athena.annotation.NotAuthentication;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Service
public class NotAuthenticationService implements InitializingBean, ApplicationContextAware {

private static final String PATTERN = "\\{(.*?)}";

public static final String ASTERISK = "*";


private ApplicationContext applicationContext;

@Getter
@Setter
private List<String> permitAllUrls = new ArrayList<>();

@Override
public void afterPropertiesSet() throws Exception {
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(x -> {
HandlerMethod handlerMethod = map.get(x);

// 获取方法上边的注解 替代path variable 为 *
NotAuthentication method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), NotAuthentication.class);
Optional.ofNullable(method).ifPresent(inner -> Objects.requireNonNull(x.getPathPatternsCondition())
.getPatternValues().forEach(url -> permitAllUrls.add(url.replaceAll(PATTERN, ASTERISK))));

// 获取类上边的注解, 替代path variable 为 *
NotAuthentication controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), NotAuthentication.class);
Optional.ofNullable(controller).ifPresent(inner -> Objects.requireNonNull(x.getPathPatternsCondition())
.getPatternValues().forEach(url -> permitAllUrls.add(url.replaceAll(PATTERN, ASTERISK))));
});
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package cn.holelin.athena.config.security;

import com.alibaba.fastjson2.JSON;
import cn.holelin.base.ResponseMessage;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(ResponseMessage.error("认证失败")));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cn.holelin.athena.config.security;

import com.alibaba.fastjson2.JSON;
import cn.holelin.base.ResponseMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(ResponseMessage.error("权限验证失败")));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package cn.holelin.athena.filter;

import cn.holelin.athena.constants.SecurityConstants;
import cn.holelin.athena.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import static cn.holelin.athena.constants.SecurityConstants.JWT_TOKEN_HEADER_KEY;

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
//check Token
if (checkJWTToken(request)) {
//解析token中的认证信息
Optional<Claims> claimsOptional = validateToken(request)
.filter(claims -> claims.get(SecurityConstants.JWT_TOKEN_ROLE_CLAIM) != null);
if (claimsOptional.isPresent()) {
List<String> authoritiesList = castList(claimsOptional.get().get(SecurityConstants.JWT_TOKEN_ROLE_CLAIM), String.class);
List<SimpleGrantedAuthority> authorities = authoritiesList
.stream().map(String::valueOf)
.map(SimpleGrantedAuthority::new).collect(Collectors.toList());
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(claimsOptional.get().getSubject(), null, authorities);
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
SecurityContextHolder.clearContext();
}
}
chain.doFilter(request, response);
}

public static <T> List<T> castList(Object obj, Class<T> clazz) {
List<T> result = new ArrayList<T>();
if (obj instanceof List<?>) {
for (Object o : (List<?>) obj) {
result.add(clazz.cast(o));
}
return result;
}
return null;
}

private Optional<Claims> validateToken(HttpServletRequest req) {
String jwtToken = req.getHeader(JWT_TOKEN_HEADER_KEY);
try {
return JwtUtil.parseAccessTokenClaims(jwtToken);
} catch (ExpiredJwtException | SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
//输出日志
return Optional.empty();
}
}

private boolean checkJWTToken(HttpServletRequest request) {
String authenticationHeader = request.getHeader(JWT_TOKEN_HEADER_KEY);
return authenticationHeader != null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
package cn.holelin.athena.utils;

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static cn.holelin.athena.constants.SecurityConstants.JWT_ALGORITHM;
import static cn.holelin.athena.constants.SecurityConstants.JWT_TOKEN_ROLE_CLAIM;

/**
* token工具类
*
*/
public class JwtUtil {


/**
* 访问令牌有效期
*/
private static final Long ACCESS_TOKEN_EXPIRE_TIME = 60 * 60 * 1000L;


/**
* 访问令牌的秘钥
*/
private static final String ACCESS_TOKEN_KEY =
"HOLELINQAZXSWEDCVFRTGBNHYUJM...ASDJLKASDNAS,DMNIUQWHEKASNDLASDOIQJWEQWEMASMD";


/**
* 刷新令牌有效期
*/
private static final Long REFRESH_TOKEN_EXPIRE_TIME = 24 * 60 * 60 * 1000L;


/**
* 刷新令牌的秘钥
*/
private static final String REFRESH_TOKEN_KEY =
"...MJUYHNBGTRFVCDEWXZAQHOLELINAJDASJDQWEULCKNHASDOIADNABDAISHDASLDKJASDUHCASDJH";

private JwtUtil() {
}

public static String getUUID() {
return UUID.randomUUID().toString().replace("-", "");
}

public static String createJWTToken(UserDetails userDetails, long timeToExpire) {
return createJWTToken(userDetails, timeToExpire,
new SecretKeySpec(ACCESS_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM));
}

public static String createAccessToken(UserDetails userDetails) {
return createJWTToken(userDetails, ACCESS_TOKEN_EXPIRE_TIME);
}

public static String createRefreshToken(UserDetails userDetails) {
return createJWTToken(userDetails, REFRESH_TOKEN_EXPIRE_TIME,
new SecretKeySpec(REFRESH_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM));
}


/**
* 根据用户信息生成一个 JWT
*
* @param userDetails 用户信息
* @param timeToExpire 毫秒单位的失效时间
* @param signKey 签名使用的 key
* @return JWT
*/
private static String createJWTToken(UserDetails userDetails, long timeToExpire,
Key signKey) {
return Jwts.builder()
//唯一ID
.id(getUUID())
.subject(userDetails.getUsername())
//权限信息
.claim(JWT_TOKEN_ROLE_CLAIM,
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
//授权信息
.issuedAt(new Date(System.currentTimeMillis()))
//过期时间
.expiration(new Date(System.currentTimeMillis() + timeToExpire))
//签名
.signWith(signKey).compact();
}


public static boolean validateAccessToken(String jwtToken) {
return validateToken(jwtToken, new SecretKeySpec(ACCESS_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM));
}

public static boolean validateRefreshToken(String jwtToken) {
return validateToken(jwtToken, new SecretKeySpec(REFRESH_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM));
}

public static boolean validateToken(String jwtToken, SecretKey signKey) {
return parseClaims(jwtToken, signKey).isPresent();
}


public static Optional<Claims> parseAccessTokenClaims(String jwtToken) {
return Optional.ofNullable(Jwts.parser().verifyWith(new SecretKeySpec(ACCESS_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM))
.build().parseSignedClaims(jwtToken).getPayload());
}

public static Optional<Claims> parseRefreshTokenClaims(String jwtToken) {
return Optional.ofNullable(Jwts.parser().verifyWith(new SecretKeySpec(REFRESH_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM))
.build().parseSignedClaims(jwtToken).getPayload());
}


public static Optional<Claims> parseClaims(String jwtToken, SecretKey signKey) {
return Optional.ofNullable(Jwts.parser().verifyWith(signKey).build().parseSignedClaims(jwtToken).getPayload());
}


public static boolean validateWithoutExpiration(String jwtToken) {
try {
Jwts.parser().verifyWith(new SecretKeySpec(ACCESS_TOKEN_KEY.getBytes(StandardCharsets.UTF_8), JWT_ALGORITHM))
.build().parseSignedClaims(jwtToken);
return true;
} catch (ExpiredJwtException | SignatureException | MalformedJwtException | UnsupportedJwtException | IllegalArgumentException e) {
if (e instanceof ExpiredJwtException) {
return true;
}
}
return false;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
package cn.holelin.athena.constants;

public class SecurityConstants {
private SecurityConstants() {
}

public static final String JWT_ALGORITHM = "HmacSHA512";
public static final String JWT_TOKEN_ROLE_CLAIM = "authorities";
public static final String JWT_TOKEN_HEADER_KEY = "token";

}

注意点

Be careful when you declare your filter as a Spring bean, either by annotating it with @Component or by declaring it as a bean in your configuration, because Spring Boot will automatically register it with the embedded container. That may cause the filter to be invoked twice, once by the container and once by Spring Security and in a different order.

在将过滤器声明为Spring bean时要小心,要么用@Component注解,要么在配置中将其声明为bean,因为Spring Boot会自动将其注册到嵌入的容器中.这可能会导致过滤器被调用两次,一次由容器调用,一次由Spring Security调用,而且调用顺序不同.

If you still want to declare your filter as a Spring bean to take advantage of dependency injection for example, and avoid the duplicate invocation, you can tell Spring Boot to not register it with the container by declaring a FilterRegistrationBean bean and setting its enabled property to false:

1
2
3
4
5
6
@Bean
public FilterRegistrationBean<TenantFilter> tenantFilterRegistration(TenantFilter filter) {
FilterRegistrationBean<TenantFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}

使用场景

  • 用户登录和认证: Spring Security可以处理用户的身份验证,包括用户名密码验证、基于数据库或LDAP的用户存储等。它提供了多种身份验证机制,如表单登录、基本认证、OAuth等。
  • 授权和权限管理: Spring Security允许定义安全规则和访问控制,以确保用户只能访问其有权访问的资源。可以使用注解、表达式或配置文件来声明和管理权限。
  • 防止跨站点请求伪造(CSRF): Spring Security可以生成和验证CSRF令牌,以防止Web应用程序受到CSRF攻击。它可以在表单中自动添加CSRF令牌,并验证提交请求中的令牌值。
  • 方法级安全性: Spring Security允许在方法级别对方法进行安全性配置。可以使用注解或表达式来定义哪些用户有权调用特定的方法。
  • 记住我功能: Spring Security提供了记住我功能,允许用户在下次访问时保持登录状态,而不需要重新输入用户名和密码。
  • 单点登录(SSO): Spring Security可以与其他身份验证和授权提供程序集成,实现单点登录功能。用户只需登录一次,即可在不同的应用程序之间共享身份验证信息。
  • 安全事件和审计日志: Spring Security可以记录安全事件和用户操作,以便进行审计和故障排查。可以配置事件监听器和审计日志记录器来记录关键的安全事件。

过滤器

  • 默认过滤器并不是直接放在Web项目的原生过滤器链中,而是通过FilterChainProxy来统一管理.
  • FilterChainProxy作为一个顶层管理者,将通过管理Security Filter.FilterChainProxy本身将通过Spring框架提供的DelegatingFilterProxy整合到原生过滤链中.

常见的过滤器

过滤器 过滤器作用
ForceEagerSessionCreationFilter 在某些情况下强制创建 HttpSession
ChannelProcessingFilter 处理 HTTPSHTTP 之间的重定向
WebAsyncManagerIntegrationFilter WebAsyncManagerSpring Security上下文集成
SecurityContextHolderFilter
SecurityContextPersistenceFilter 在处理请求之前,将安全信息加载到SecurityContextHolder中以方便后续使用.请求结束后再擦除SecurityContextHolder中的信息
HeaderWriterFilter 头信息加入到响应中
CorsFilter 处理跨域问题
CsrfFilter 防止 CSRF 攻击
LogoutFilter 注销当前用户
OAuth2AuthorizationRequestRedirectFilter 处理OAuth2认证重定向
Saml2WebSsoAuthenticationRequestFilter 处理SAML认证
X509AuthenticationFilter 处理X509认证
AbstractAuthenticationProcessingFilter 基于浏览器的http认证请求的抽象处理器。
CasAuthenticationFilter 处理CAS单点登录
OAuth2LoginAuthenticationFilter 处理OAuth2认证
Saml2WebSsoAuthenticationFilter 处理SAML认证
UsernamePasswordAuthenticationFilter 处理表单登录
DefaultLoginPageGeneratingFilter 生成默认的登录页面
DefaultLogoutPageGeneratingFilter 生成默认的注销页面
ConcurrentSessionFilter 处理Session有效期
DigestAuthenticationFilter 处理HTTP摘要认证
BearerTokenAuthenticationFilter 处理OAuth2认证时的Access Token
BasicAuthenticationFilter 处理HttpBasic登录
RequestCacheAwareFilter 请求缓存支持
SecurityContextHolderAwareRequestFilter 将请求包装成对 SecurityContext 的支持
JaasApiIntegrationFilter JAAS 集成
RememberMeAuthenticationFilter 使用“记住我”身份验证
AnonymousAuthenticationFilter 匿名身份验证支持
OAuth2AuthorizationCodeGranttFilter 处理OAuth2认证的授权码
SessionManagermentFilter 处理Session并发问题
ExceptionTranslationFilter 处理异常并执行相应操作
FilterSecurityInterceptor 主要做认证和授权拦截,新版被AuthorizationFilter代替
AuthorizationFilter 主要做认证和授权拦截
SwitchUserFilter 处理账户切换

源码分析

核心组件

  • org.springframework.security.web.FilterChainProxy

    FilterChainProxy可以认为是整个Spring Security处理请求的一个起点,如果你遇到Security相关问题,又不清楚是具体哪个Filter导致的,就可以从这里开始Debug.

  • org.springframework.security.web.SecurityFilterChain

  • 查看过滤器顺序org.springframework.security.config.annotation.web.builders.FilterOrderRegistration

  • 异常处理Filter org.springframework.security.web.access.ExceptionTranslationFilter

    img

    • 图片来源于https://docs.spring.io/spring-security/reference/servlet/architecture.html

认证Authentication

核心组件

  • SecurityContextHolder - SecurityContextHolder 是 Spring Security 存储经过身份验证的详细信息的位置.

    img

  • SecurityContext - 从 SecurityContextHolder 获取,包含当前经过身份验证的用户的 Authentication .

  • Authentication - 可以是 AuthenticationManager 的输入,以提供用户提供的用于身份验证的凭据或来自 SecurityContext 的当前用户.

  • GrantedAuthority - 授予 Authentication 上主体的权限(即角色、范围等)

  • AuthenticationManager

    • 它的入参和出参都是Authentication对象。
    • 通常情况下,入参提供了必要的认证信息,例如用户名和密码。而在认证成功后,该方法会返回认证结果,并附加认证状态,用户拥有的权限列表等信息。
    • 如果认证失败,它会抛出AuthenticationException异常类的子类,其中包括DisabledException,LockedException和BadCredentialsException等账号相关的异常
  • AuthenticationProvider - 身份验证提供程序是实际执行身份验证的组件。它从用户存储源(如数据库、LDAP等)中获取用户信息,并进行密码比对或其他验证方式,确定用户的身份是否有效。Spring Security提供了多种身份验证提供程序的实现,如基于数据库的验证、基于LDAP的验证、OpenID验证等,由 ProviderManager 使用来执行特定类型的身份验证.

  • ProviderManager - AuthenticationManager 最常见的实现.

  • 使用 AuthenticationEntryPoint 请求凭证 - 用于从客户端请求凭证(即重定向到登录页面、发送 WWW-Authenticate 响应等)

  • AbstractAuthenticationProcessingFilter - 用于身份验证的基础 Filter .

Spring Security中,很多初学者都容易混淆RoleAuthority的区别,实际上在技术实现层面上,这两者没有本质区别,底层都仅仅是一个表示权限的字符串标识符.更多的区别在于权限管理的概念上,一般情况下,Authority表示细粒度的操作权限,比如ADD_USER,DELETE_USER等,通常是动词;而Role则会与实际业务角色想对应,比如管理员ADMIN,普通员工STAFF等,通常是名称.此外,一般一个Role会对应多个Authority,同时角色之间可以存在继承关系,比如ADMIN可以继承STAFF的所有权限

1
2
3
4
5
6
AbstractAuthenticationProcessingFilter#doFilter
-->子类(UsernamePasswordAuthenticationFilter)的attemptAuthentication-->AuthenticationManager
-->子类(ProviderManager)
-->AuthenticationProvider#authenticate
-->AbstractUserDetailsAuthenticationProvider#retrieveUser
-->DaoAuthenticationProvider#retrieveUser
  • org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

// 判断当前filter是否可以处理当前请求,若不行,则交给下一个filter去处理.
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}

Authentication authResult;

try {
// 最终调用子类的attemptAuthentication方法
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 最终认证成功后,会处理一些与session相关的方法(比如将认证信息存到session等操作).
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// Authentication success
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
/*
* 最终认证成功后的相关回调方法,主要将当前的认证信息放到SecurityContextHolder中
* 并调用成功处理器做相应的操作.
*/
successfulAuthentication(request, response, chain, authResult);
}
  • org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 // org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication	
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 认证请求的方式必须为POST
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}
  • org.springframework.security.authentication.ProviderManager#authenticate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
// org.springframework.security.authentication.ProviderManager#authenticate

/**
* Attempts to authenticate the passed {@link Authentication} object.
* <p>
* The list of {@link AuthenticationProvider}s will be successively tried until an
* <code>AuthenticationProvider</code> indicates it is capable of authenticating the
* type of <code>Authentication</code> object passed. Authentication will then be
* attempted with that <code>AuthenticationProvider</code>.
* <p>
* If more than one <code>AuthenticationProvider</code> supports the passed
* <code>Authentication</code> object, the first one able to successfully
* authenticate the <code>Authentication</code> object determines the
* <code>result</code>, overriding any possible <code>AuthenticationException</code>
* thrown by earlier supporting <code>AuthenticationProvider</code>s.
* On successful authentication, no subsequent <code>AuthenticationProvider</code>s
* will be tried.
* If authentication was not successful by any supporting
* <code>AuthenticationProvider</code> the last thrown
* <code>AuthenticationException</code> will be rethrown.
*
* @param authentication the authentication request object.
*
* @return a fully authenticated object including credentials.
*
* @throws AuthenticationException if authentication fails.
*/
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();

for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}

if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}

try {
// 调用Provider的方法
result = provider.authenticate(authentication);

if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}

if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}

if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}

// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}

// Parent was null, or didn't authenticate (or throw an exception).

if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}

// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}

throw lastException;
}
  • org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
 // org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider#authenticate
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));

// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();

boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);

if (user == null) {
cacheWasUsed = false;

try {
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");

if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}

Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}

try {
/*
* 前检查由DefaultPreAuthenticationChecks类实现(主要判断当前用户是否锁定,过期,冻结
* User接口)
*/
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (AuthenticationException exception) {
if (cacheWasUsed) {
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
cacheWasUsed = false;
user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user,
(UsernamePasswordAuthenticationToken) authentication);
}
else {
throw exception;
}
}
// 检测用户密码是否过期
postAuthenticationChecks.check(user);

if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}

Object principalToReturn = user;

if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}

return createSuccessAuthentication(principalToReturn, authentication, user);
}
  • org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* Core interface which loads user-specific data.
* <p>
* It is used throughout the framework as a user DAO and is the strategy used by the
* {@link org.springframework.security.authentication.dao.DaoAuthenticationProvider
* DaoAuthenticationProvider}.
*
* <p>
* The interface requires only one read-only method, which simplifies support for new
* data-access strategies.
*
* @see org.springframework.security.authentication.dao.DaoAuthenticationProvider
* @see UserDetails
*
* @author Ben Alex
*/
public interface UserDetailsService {
// ~ Methods
// ========================================================================================================

/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

默认生成的生成的用户名和密码如何生成

  • 查看UserDetailsServiceAutoConfiguration此类

  • org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration#getOrDeducePassword

授权Authorization

The first thing this provides is an enhanced set of authorization fields and methods to your SpEL expressions. What follows is a quick overview of the most common methods:

  • permitAll - The request requires no authorization to be invoked; note that in this case, the Authentication is never retrieved from the session
    • 允许所有
  • denyAll - The request is not allowed under any circumstances; note that in this case, the Authentication is never retrieved from the session
    • 拒绝所有
  • hasAuthority - The request requires that the Authentication have a GrantedAuthority that matches the given value
    • 根据指定的权限进行判断
  • hasRole - A shortcut for hasAuthority that prefixes ROLE_ or whatever is configured as the default prefix
    • 根据指定的角色进行判断
  • hasAnyAuthority - The request requires that the Authentication have a GrantedAuthority that matches any of the given values
    • 匹配任意权限
  • hasAnyRole - A shortcut for hasAnyAuthority that prefixes ROLE_ or whatever is configured as the default prefix
    • 匹配任意角色
  • hasPermission - A hook into your PermissionEvaluator instance for doing object-level authorization

异常处理

  • 异常处理Filter org.springframework.security.web.access.ExceptionTranslationFilter

  • 该配置类提供了两个实用接口:

    • **AuthenticationEntryPoint**该类用来统一处理 AuthenticationException 异常,用来处理认证异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      import com.alibaba.fastjson2.JSON;
      import cn.holelin.athena.base.ResponseMessage;
      import org.springframework.http.MediaType;
      import org.springframework.security.core.AuthenticationException;
      import org.springframework.security.web.AuthenticationEntryPoint;

      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.nio.charset.StandardCharsets;

      public class ResourceAuthExceptionEntryPoint implements AuthenticationEntryPoint {
      @Override
      public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
      response.setCharacterEncoding(StandardCharsets.UTF_8.name());
      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
      final PrintWriter writer = response.getWriter();
      writer.write(JSON.toJSONString(ResponseMessage.error("账号认证失败,请检查Token")));
      writer.flush();
      writer.close();
      }
      }
    • AccessDeniedHandler 该类用来统一处理 AccessDeniedException 异常,用来处理授权异常

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      import com.alibaba.fastjson2.JSON;
      import cn.holelin.athena.base.ResponseMessage;
      import org.springframework.http.MediaType;
      import org.springframework.security.access.AccessDeniedException;
      import org.springframework.security.web.access.AccessDeniedHandler;

      import javax.servlet.ServletException;
      import javax.servlet.http.HttpServletRequest;
      import javax.servlet.http.HttpServletResponse;
      import java.io.IOException;
      import java.io.PrintWriter;
      import java.nio.charset.StandardCharsets;

      public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
      @Override
      public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
      response.setCharacterEncoding(StandardCharsets.UTF_8.name());
      response.setStatus(HttpServletResponse.SC_FORBIDDEN);
      response.setContentType(MediaType.APPLICATION_JSON_VALUE);
      final PrintWriter writer = response.getWriter();
      writer.write(JSON.toJSONString(ResponseMessage.error("账号权限失败,请检查账号权限")));
      writer.flush();
      writer.close();
      }
      }

Spring Security创建使用session的方法

  • Spring Security提供4种方式精确的控制会话的创建:

    • always: 如果当前请求没有session存在,Spring Security创建一个session。

    • ifRequired(默认): Spring Security在需要时才创建Session

    • never: Spring Security将永远不会主动创建session,但是如果session已经存在,它将使用该session

    • stateless: Spring Security不会创建或使用任何session。适合于接口型的无状态应用,该方式节省资源。