Security - 源码与流程
# Spring Security过滤器
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于 处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个「仓储」中,然后将 SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login
的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的 usernameParameter 和 passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
(14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember-me 的 cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
# SpringSecurity基本流程
Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)
方法中配置,没有配置不生效。我们重点要认识以下三个过滤器:
UsernamePasswordAuthenticationFilter
过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。
ExceptionTranslationFilter
过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor
过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由 ExceptionTranslationFilter
过滤器进行捕获和处理。
# Spring Security认证流程
# UsernamePasswordAuthenticationFilter过滤器概念
UsernamePasswordAuthenticationFilter
:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login
的请求。从表单中获取用户名和密码 时,默认使用的表单 name 值为 username
和 password
,这两个值可以通过设置这个过滤器的 usernameParameter
和 passwordParameter
两个参数的值进行修改。
# 图例流程(具体看源码,对应步骤看更好理解)
当用户进行表单登录,且提交为 post 时,表单信息会经过
UsernamePasswordAuthenticationFilter
过滤器,进行身份验证过滤器使用
attempAuthentication
方法进行身份认证,认证成功或失败后返回Authentication
对象,认证成功的话,该对象封装了用户信息attempAuthentication(身份封装方法)过程:
获取表单的用户名和密码,默认是 username 和 password,以及 post 提交,不是这些请求会抛出异常
将 username 和 password 进行封装成
Authentication
对象,此时该对象是 未认证 的,Authentication
类也包括了请求信息,如sessionId
等Authentication 类
Collection<? extends GrantedAuthority> getAuthorities();// 用户的权限集合 // 用户的密码 Object getCredentials(); // 请求携带的一些属性信息,如 sessionId,remoteAddress Object getDetails(); // 未认证时为前端传来的用户名,认证成功后为封装认证用户信息的 UserDetails 对象 Object getPrincipal(); // 是否被认证了(true:认证成功,false:认证失败) boolean isAuthenticated(); //设置是否被认证了(true:已认证,false:未认证) void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
1
2
3
4
5
6
7
8
9
10
11调用
ProviderManager
类的authenticate()
方法,传入 未认证 的Authentication
对象,进行认证 (UsernamePasswordAuthenticationToken
是Authentication
的子类),--认证成功 后返回Authentication
对象,该对象封装了用户信息,内容如下:// 使用前端传来的 username 和 password 构造 Authentication 对象,标记该对象未认证 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // 该方法内部将 request 请求的内容封装到 Authentication 对象里,如 sessionId setDetails(request, authRequest); // 调用 ProviderManager 类的 authenticate() 方法,传入未认证的 Authentication 对象,进行认证 return this.getAuthenticationManager().authenticate(authRequest);
1
2
3
4
5
6UsernamePasswordAuthenticationToken 类(两个构造器)
第一个构造器用于封装未认证的用户信息,上方代码块的构造器就是这个
public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); }
1
2
3
4
5
6第二个构造器用于封装认证成功的用户信息
public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override }
1
2
3
4
5
6
7
ProviderManager 类的 authenticate(身份认证方法)过程
获取传入的
Authentication
对象,即UsernamePasswordAuthenticationToken.class
封装的对象获取认证方式列表的迭代器(即官方或者用户自定义的认证方式都会放在这个迭代器里)
判断迭代器的认证方式是否适用传来的
Authentication
对象是,则 委托
DaoAuthenticationProvider
的authenticate
方法进行验证Authentication result = null; // ...... result = provider.authenticate(authentication); // result 是 Authentication 类的对象
1
2
3- 该方法内部 关联
UserDetailsService
类,该类的一个方法(loadUserByUsername
)去数据库查询用户是否存在 - 存在则封装为
UserDetails
接口的User
实现类对象,并 返回 该User
对象 - 返回到
ProviderManager
时,再次把 User 对象 封装 为 已认证 的Authentication
对象,就是上方代码块的result
- 该方法内部 关联
成功,则返回一个 已认证 的
Authentication
对象(上方的User
类的再次封装),将之前传入的 未认证 的Authentication
对象的details
信息(用户权限信息等)拷贝一份到 已认证 的Authentication
对象中失败,适用父类
AuthenticationManager
进行验证,如果还是失败,则返回失败的异常信息成功后,返回
Authentication
对象,对应上方的 --认证成功,再返回到过滤器中
执行完
attempAuthentication
方法后,获得Authentication
对象,并调用doFilter
方法,进入AbstractAuthenticationProcessingFilter
类中,该类处理认证成功或者认证失败的后续动作。
# SpringSecurity权限访问流程
# AbstractAuthenticationProcessingFilter过滤器
处理认证成功或者认证失败的后续动作,并且将认证成功的用户信息存入上下文里,框架随时根据上下文的用户信息进行权限控制
如果认证成功,doFilter
方法调用一个方法进入认证成功的处理器:sucessfulAuthentication
方法,失败则调用另一个方法进入认证失败的处理器:unsucessfulAuthentication
方法
sucessfulAuthentication 方法
- 将认证成功的用户信息对象
Authentication
对象封装到SecurityContext
对象里,然后该对象存入SecurityContextHolder
上下文里 - rememberMe 的处理
- 发布认证成功的事件
- 调用认证成功的处理器方法:
onAuthenticationSuccess
,该方法在AuthenticationSuccessHandler
类里,需要我们继承实现
unsucessfulAuthentication方法
- 清除该线程在
SecurityContextHolder
中对应的SecurityContext
对象 - rememberMe 的处理
- 调用认证失败的处理器方法:
onAuthenticationFailure
,该方法在AuthenticationFailureHandler
类里,需要我们继承实现
# SecurityContextPersistenceFilter过滤器
前面提到过,在 UsernamePasswordAuthenticationFilter 过滤器认证成功之后,会在 AbstractAuthenticationProcessingFilter 过滤器的认证成功的处理方法中将已认证的用户信息对象 Authentication 封装进 SecurityContext,并存入 SecurityContextHolder。之后,响应会通过 SecurityContextPersistenceFilter 过滤器。
该过滤器的位置在所有过滤器的最前面,请求到来先进它,响应返回最后一个通过它,所以在该过滤器中处理已认证的用户信息对象 Authentication 与 Session 绑定。认证成功的响应通过 SecurityContextPersistenceFilter 过滤器时,会从 SecurityContextHolder 中取出封装了已认证用户信息对象 Authentication 的 SecurityContext,放进 Session 中。当请求再次到来时,请求首先经过该过滤器,该过滤器会判断当前请求的 Session 是否存有 SecurityContext 对象,如果有则将该对象取出再次放入 SecurityContextHolder 中,之后该请求所在的线程获得认证用户信息,后续的资源访问不需要进行身份认证;当响应再次返回时,该过滤器同样从 SecurityContextHolder 取出 SecurityContext 对象,放入 Session 中。
# ExceptionTranslationFilter过滤器
该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理,例如:权限访问限制
# FilterSecurityInterceptor 过滤器
FilterSecurityInterceptor 是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器
# JWT流程
# 代码实现一(简单)
登录时,在 Controller 获得用户名和密码,调用 Service 层的登录方法
@RequestMapping(value = "/authentication/login", method = RequestMethod.POST) public String createToken( String username,String password ) throws AuthenticationException { return authService.login( username, password ); // 登录成功会返回 JWT Token 给用户 }
1
2
3
4service 层先把用户名和密码封装为
UsernamePasswordAuthenticationToken
对象,然后使用AuthenticationManager
的authenticate
方法传入该对象,进行验证,返回值为Authentication
对象,并把该对象存入上下文里,接着去数据库根据用户名获取用户信息和角色信息,存入UserDetails
对象并返回,接着根据对象生成 token,返回给前端@Autowired private AuthenticationManager authenticationManager; public String login(String username, String password) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password ); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername( username ); final String token = jwtTokenUtil.generateToken(userDetails); return token; }
1
2
3
4
5
6
7
8
9
10写一个工具类,用于生成 token,根据 token 获取用户信息、时长,刷新 token,验证 token 是否过期等操作
public class JwtTokenUtils implements Serializable { private static final long serialVersionUID = -5625635588908941275L; private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; // 生成 token public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } // 验证 token public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return !isTokenExpired(token); } // 刷新 token public String refreshToken(String token){ Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED,new Date()); return generateToken(claims); } // 验证 token 是否失效 public boolean isTokenExpired(String token){ Date expireDate = getExpiredDateFromToken(token); return expireDate.before(new Date()); } // 从 token 中获取过期时间 public Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } // 从 token 中获取用户名 public String getUserNameFromToken(String token){ String username; try{ Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } // 从 token 中获取荷载 private Claims getClaimsFromToken(String token){ Claims claims = null; try { claims = Jwts.parser() .setSigningKey(Const.SECRET) .parseClaimsJws(token) .getBody(); } catch (Exception e){ e.printStackTrace(); } return claims; } // 生成过期时间 private Date generateExpirationDate() { return new Date(System.currentTimeMillis()+Const.EXPIRATION_TIME*1000); } // 根据荷载生成 token String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, Const.SECRET ) .compact(); } }
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写一个过滤器,每次用户访问页面都会获取请求头,判断请求题是否有 token,token 是否过期,如果都符合,则从 token 获取用户名,去数据库获取用户的信息,并封装为
UsernamePasswordAuthenticationToken
对象存入上下文,只有存入上下文,security 框架才不会拦截public class Const { public static final long EXPIRATION_TIME = 432_000_000; // 5天(以毫秒ms计) public static final String SECRET = "CodeSheepSecret"; // JWT密码 public static final String TOKEN_PREFIX = "Bearer"; // Token前缀 public static final String HEADER_STRING = "Authorization"; // 存放Token的Header Key }
1
2
3
4
5
6
7
8public class JwtFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtils jwtTokenUtil; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { // 确认是否能根据key拿到value String authHeader = httpServletRequest.getHeader( Const.HEADER_STRING ); // 判断登录用户的token不为空和是Bearer开头的 if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) { // 取到token final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() ); // 从用户请求携带的token获取用户名,能取到证明token除了时间以外都合法了 String username = jwtTokenUtil.getUserNameFromToken(authToken); // token 存在用户名但没有认证的 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); // 根据userDetails验证了token是否有效(验证时间是否过期和当前用户名是否匹 配) if (jwtTokenUtil.validateToken(authToken, userDetails)) { // 我们的token,框架是不认识的,token有效就转化构建 UsernamePasswordAuthenticationToken表示认证通过和进行相关授权 UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( httpServletRequest)); // 设置了认证主体,到UsernamePasswordAuthenticationFilter就不会拦 截,因为你应该带有了它的token SecurityContextHolder.getContext().setAuthentication(authentication); } } } // 继续执行其他过滤器 filterChain.doFilter(httpServletRequest, httpServletResponse); } }
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在核心配置类,把步骤 4 的过滤器放入 security 的过滤器之前,这样 security 不会拦截
protected void configure( HttpSecurity httpSecurity ) throws Exception { // ...... httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); }
1
2
3
4
5
6
# 代码实现二(推荐)
代码移至 Security - JWT登录实战 处,该内容提供的 JWT 和 Spring Security 工具类,更加具有通用性,适合各种项目,可以收藏。
当然,为了方便获取通用工具类,这里也写出代码,具体如何使用,就请看移至的内容。
JwtTokenUtils 类封装了 JWT 的各种操作,包括生成 token,解析 token,刷新 token,判断 token 是否过期等操作,是一个通用的类
package cn.kbt.util;
import cn.kbt.security.JwtAuthenticatioToken;
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 org.springframework.security.core.authority.SimpleGrantedAuthority;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.*;
/**
* @author Young Kbt
* @date 2021/12/24 15:49
* @description JWT 工具类
*/
public class JwtTokenUtils implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名称,可自定义,或者获取官方提供
*/
private static final String USERNAME = Claims.SUBJECT;
/**
* 创建时间
*/
private static final String CREATED = "created";
/**
* 权限列表
*/
private static final String AUTHORITIES = "authorities";
/**
* 密钥,自定义,根据密钥生成 token,或还原 token
*/
private static final String SECRET = "abcdefgh";
/**
* 有效期 12 小时
*/
private static final long EXPIRE_TIME = 12 * 60 * 60 * 1000;
/**
* 生成令牌
* @param authentication 认证信息
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
/**
* 从令牌中获取用户名
* @param token 令牌
* @return 用户名
*/
public static String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 根据请求令牌获取登录认证信息
* @param request 客户端的请求
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
// 如果请求令牌不为空
if (token != null) {
// 如果在 Security 上下文检测没有登录过
if (SecurityUtils.getAuthentication() == null) {
// 根据 token 获取曾经登录的数据证明
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return null;
}
String username = claims.getSubject();
if (username == null) {
return null;
}
// 如果 token 过期
if (isTokenExpired(token)) {
return null;
}
// 获取用户的权限列表
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// 如果用户权限是集合,则存入新的集合里
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new SimpleGrantedAuthority((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
// 如果上下文有用户登录过,则检查是否是当前用户
if (validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中 Authentication 非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}
/**
* 从令牌中获取数据声明
* @param token 令牌
* @return 数据声明
*/
private static Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 验证令牌
* @param token 令牌
* @param username 用户名
* @return 令牌是否正确
*/
public static Boolean validateToken(String token, String username) {
String userName = getUsernameFromToken(token);
return (userName.equals(username) && !isTokenExpired(token));
}
/**
* 刷新令牌
* @param token 令牌
* @return 新的令牌
*/
public static String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(CREATED, new Date());
refreshedToken = generateToken(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 判断令牌是否过期
* @param token 令牌
* @return 是否过期
*/
public static Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return false;
}
}
/**
* 根据请求获取 token
* @param request 用户请求
* @return 令牌
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if (token == null) {
token = request.getHeader("token");
} else if (token.contains(tokenHead)) {
// 把 Bearer 去掉,只要后面的 token 值
token = token.substring(tokenHead.length());
}
if ("".equals(token)) {
token = null;
}
return token;
}
}
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
202
203
204
205
206
207
208
SecurityUtils 类封装了 Spring Security 相关的操作,如认证 token,登录验证,获取登录信息等操作,也是一个通用的类。
package cn.kbt.util;
import cn.kbt.security.JwtAuthenticatioToken;
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;
/**
* @author Young Kbt
* @date 2021/12/24 15:50
* @description
*/
public class SecurityUtils {
/**
* 系统登录认证
* @param request 客户端请求
* @param username 当前用户名
* @param password 当前用户密码
* @param authenticationManager 认证对象
* @return 认证后的用户信息,内容包括 token
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
// token 存入 request 的相关信息
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程,获取调用 UserDetailsServiceImpl 的 loadUserByUsername 方法
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
/**
* 获取令牌进行认证
* @param request 客户端请求
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
/**
* 从上下文获取当前用户名
* @return 当前用户名
*/
public static String getUsername() {
String username = null;
Authentication authentication = getAuthentication();
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
return String.valueOf(principal);
}
}
return username;
}
/**
* 从传入的认证信息中获取用户名
* @return 用户名
*/
public static String getUsername(Authentication authentication) {
String username = null;
if (authentication != null) {
Object principal = authentication.getPrincipal();
if (principal != null && principal instanceof UserDetails) {
username = ((UserDetails) principal).getUsername();
} else {
return String.valueOf(principal);
}
}
return username;
}
/**
* 从上下文获取当前登录信息
* @return 当前登录信息
*/
public static Authentication getAuthentication() {
if (SecurityContextHolder.getContext() == null) {
return null;
}
return SecurityContextHolder.getContext().getAuthentication();
}
}
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
# 总结
# 常用 Spring Security 项目要写的类及流程
登录时,与数据库交互,判断登录是否成功的类(自定义类继承
UserDetailsService
,重写loadUserByUsername
方法)登录成功后的处理类(自定义类继承
AuthenticationSuccessHandler
,重写onAuthenticationSuccess
方法)登录失败后的处理类(自定义类继承
AuthenticationFailureHandler
,重写onAuthenticationFailure
方法)退出登录时的处理类(自定义类继承
LogoutHandler
,重写logout
方法)退出登录成功后的处理类(自定义类继承
onLogoutSuccess
,重写onLogoutSuccess
方法),第四步和这一步其实可以选择一个写即可,一般退出登录都会成功未授权统一处理类(自定义类继承
AuthenticationEntryPoint
,重写commence
方法)最重要的类,配置核心类内容:
自定义类继承
WebSecurityConfigurerAdapter
重写
configure(HttpSecurity http)
,统一把前面的步骤放入 Spring Security 框架重写
configure(AuthenticationManagerBuilder auth)
,把第一步与数据库交互的类放入 Spring Security 框架,框架自动与数据库交互重写
configure(WebSecurity web)
方法,设置不进行认证的路径设置,这些路径都可以直接访问,一般是静态资源
# 需要 JWT 的 Spring Security 项目要写的类及流程
自定义 Token 管理类,根据用户信息生成 Token ,解析 Token 获取用户信息或者 Token 时长,删除 Token
与上面 Spring Security 项目流程一样,只不过需要在登录成功后的处理类中,使用 Token 管理类,生成 Token,存入 redis 数据库或其他地方,退出登录时的处理类或者退出登录成功后的处理类中,删除 Token,以及 redis 数据库或者其他地方存有的 Token
用户登录成功,记得在 Controller 的方法将生产 Token 返回给客户端,否则下一次客户端无法携带 Token 访问项目资源,导致登录成功却没有权限
# 什么时候返回 Token?
顺便补充上面「常用 Spring Security 项目要写的类及流程」的第 2 点。
# 方式一(推荐)
登录成功后的处理类可以生成并返回 token
/**
* @author Kele-Bingtang
* @date 2022/12/10 23:45
* @note 登录认证成功处理器类
*/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// 设置客户端的响应的内容类型
response.setContentType("application/json;charset=UTF-8");
// 获取当登录用户信息
User user = (User) authentication.getPrincipal();
// 生成 token
String token = JwtTokenUtils.generateToken(authentication);
LOGGER.info("用户 {} 登录成功,token 为 {}", user.getUsername(), token);
// 获取输出流
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.println(new ObjectMapper().writeValueAsString(HttpResult.ok(token)));
writer.flush();
writer.close();
}
}
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
如果失败,也是类似,自定义一个类实现 AuthenticationFailureHandler
,重写 onAuthenticationFailure
方法。如:
/**
* @author Kele-Bingtang
* @date 2022/12/10 23:51
* @note 用户认证失败处理类
*/
public class LoginFailureHandler implements AuthenticationFailureHandler {
private final Logger LOGGER = LoggerFactory.getLogger(this.getClass());
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// 设置客户端响应编码格式
response.setContentType("application/json;charset=UTF-8");
// 获取输出流
PrintWriter writer = response.getWriter();
// 判断异常类型
ResponseStatusEnum responseStatusEnum = ResponseStatusEnum.LOGIN_FAIL;
if (exception instanceof AccountExpiredException) {
responseStatusEnum = ResponseStatusEnum.USER_ACCOUNT_EXPIRED;
} else if (exception instanceof BadCredentialsException) {
responseStatusEnum = ResponseStatusEnum.USERNAME_PASSWORD_ERROR;
} else if (exception instanceof CredentialsExpiredException) {
responseStatusEnum = ResponseStatusEnum.USER_PASSWORD_EXPIRED;
} else if (exception instanceof DisabledException) {
responseStatusEnum = ResponseStatusEnum.USER_ACCOUNT_DISABLE;
} else if (exception instanceof LockedException) {
responseStatusEnum = ResponseStatusEnum.USER_ACCOUNT_LOCKED;
} else if (exception instanceof InternalAuthenticationServiceException) {
responseStatusEnum = ResponseStatusEnum.USER_ACCOUNT_NOT_EXIST;
}
LOGGER.error("Exception:{}", responseStatusEnum.getMessage());
// 将错误信息转换成 JSON
writer.println(new ObjectMapper().writeValueAsString(HttpResult.processResult(null, responseStatusEnum)));
writer.flush();
writer.close();
}
}
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
HttpResult 是响应类,processResult 方法的第一个参数是 data,第二个参数是状态。
状态用枚举类封装:
/**
* @author Young Kbt
* @date 2022/4/30 15:19
* @note 响应状态枚举类
*/
public enum ResponseStatusEnum {
SUCCESS(200, "success", "操作成功!"),
FAIL(600, "fail", "操作失败!"),
ERROR(700, "error", "操作错误!"),
VALIDATION_ERROR(1001, "error", "传递的参数不符合要求"),
CONDITION_SQL_ERROR(1002, "error", "字段不存在"),
LOGIN_FAIL(1003, "fail", "登录失败"),
USER_REGISTER_FAILED(1004, "fail", "注册失败"),
USER_ACCOUNT_EXISTED(1005,"fail", "用户名已存在"),
USER_ACCOUNT_EXPIRED(1006,"fail","账号过期"),
USERNAME_PASSWORD_ERROR(1007,"fail","用户名或密码错误"),
USER_PASSWORD_EXPIRED(1008,"fail","账号过期"),
USER_ACCOUNT_DISABLE(1009,"fail","账号禁用"),
USER_ACCOUNT_LOCKED(1010,"fail","账号锁定"),
USER_ACCOUNT_NOT_EXIST(1011,"fail","账号不存在");
private Integer code;
private String status;
private String message;
private ResponseStatusEnum(Integer code, String status, String message) {
this.code = code;
this.status = status;
this.message = message;
}
// setter getter ...
}
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
# 方式二
UsernamePasswordAuthenticationFilter
是 认证前 经过的拦截器,这个拦截器执行后,将会调用 loadUserByUsername
方法进行认证,接着触发登录成功或者失败的处理类,那么我们可以自定义类重写该过滤器,在原有的代码上加上一些友好的返回提示,并且调用 loadUserByUsername
认证成功后,返回 token(请求头加入 token)。
如:
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RsaKeyProperties prop;
public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
this.authenticationManager = authenticationManager;
this.prop = prop;
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
return authenticationManager.authenticate(authRequest);
}catch (Exception e){
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
resultMap.put("msg", "用户名或密码错误!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
throw new RuntimeException(e);
}
}
public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
UserPojo user = new UserPojo();
user.setUsername(authResult.getName());
user.setRoles((List<RolePojo>)authResult.getAuthorities());
// 生成 token
String token = JwtTokenUtils.generateToken(authResult);
response.addHeader("Authorization", "Bearer " + token);
try {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", HttpServletResponse.SC_OK);
resultMap.put("msg", "认证通过!");
out.write(new ObjectMapper().writeValueAsString(resultMap));
out.flush();
out.close();
}catch (Exception outEx){
outEx.printStackTrace();
}
}
}
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
# 方式三
在 Controller 返回 token
@RestController
public class JwtAuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@PostMapping("/login")
public HttpResult login(@RequestParam("username") String username, @RequestParam("password") String password, HttpServletRequest request) {
User user = userService.findByUsername(username);
// 账号不存在
if (user == null) {
return HttpResult.error("账号不存在");
// 数据库的密码是加密过的
}else if(!bCryptPasswordEncoder.matches(password,user.getPassword())) {
return HttpResult.error("密码不正确");
}
// 该工具类是进行权限认证,将用户存入 Security 里
JwtAuthenticationToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}
}
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