现在前后端分离的Web应用越来越流行,后端提供一系列API,前端通过调用这些API完成系统的各个需求的呈现。那么这篇文章主要讲一讲在前后端分离的开发场景中,后端使用Spring Security如何实现登录以及API请求时的身份和权限校验。
Spring Security的核心就是各种Filter的运用,我们需要设计两个主要的Filter
LoginAuthenticationFilter
POST
的登录请求JwtTokenAuthenticationFilter
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.3version>
parent>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
dependencies>
为了便于大家理解,我尽量让演示代码精简。定义一个类继承WebSecurityConfigurerAdapter
,完整代码如下:
@Component
public class DemoWen3Security extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 需要忽略的请求,不校验,直接放行
.requestMatchers(new AntPathRequestMatcher("/**/test")).permitAll()
.anyRequest().authenticated();
// 登录过滤器
http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// token校验过滤器
http.addFilterBefore(new JwtTokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 禁用表单提交
http.formLogin().disable();
// 禁用注销
http.logout().disable();
// 禁用csrf功能,方便使用curl或postman测试
http.csrf().disable();
}
}
代码中已经有关键注释对逻辑进行说明,完成代码如下:
public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public LoginAuthenticationFilter() {
// 根据需要自定义处理登录请求的Action
setFilterProcessesUrl("/user/login");
// 校验用户名密码
setAuthenticationManager(new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken)authentication;
if("xxx".equals(token.getPrincipal()) &&
"xxx".equals(token.getCredentials())) {
return new TestingAuthenticationToken("xxx", "xxx");
}
throw new BadCredentialsException("用户名密码错误");
}
});
// 登录成功处理逻辑
setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.getWriter().write("login successfully, token:" + UUID.randomUUID().toString());
}
});
// 登录失败处理逻辑
setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.getWriter().write("login fail:" + exception.getMessage());
}
});
}
}
代码中已经有关键注释对逻辑进行说明,完成代码如下:
public class JwtTokenAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
public JwtTokenAuthenticationFilter() {
// token校验逻辑
setAuthenticationManager(new AuthenticationManager() {
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
TestingAuthenticationToken token = (TestingAuthenticationToken)authentication;
String accessToken = (String)token.getCredentials();
// 校验token逻辑,这里演示只是判空
if(StringUtils.isNoneBlank(accessToken)) {
return new TestingAuthenticationToken("xxx", "xxx", AuthorityUtils.createAuthorityList(new String[]{"admin"}));
}
throw new BadCredentialsException("token is empty");
}
});
// 校验失败处理逻辑
setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("401 token is invalid:" + exception.getMessage());
}
});
}
// 获取token并触发校验
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从请求头获取token
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if(StringUtils.isBlank(token)) {
throw new BadCredentialsException("token is empty");
}
TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken(token, token);
return this.getAuthenticationManager().authenticate(testingAuthenticationToken);
}
// 判断当前请求是否需要进行token校验
@Override
protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
// 不需要校验的请求直接忽略放行
if(new AntPathRequestMatcher("/**/test").matches(request)) {
return false;
}
// 如果已经校验通过,不重复校验
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(null!=authentication && authentication.isAuthenticated()) {
return false;
}
return true;
}
// 校验通过后的处理逻辑
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
// 把身份数据放入上下文,后续代码可以随时通过静态方法获取SecurityContextHolder.getContext().getAuthentication()
SecurityContextHolder.getContext().setAuthentication(authResult);
// 继续往下进入controller或其它被访问的资源
chain.doFilter(request, response);
}
}
@SpringBootApplication
public class DemoSpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(DemoSpringSecurityApplication.class, args);
}
}
curl -ik -XPOST "http://localhost:8080/user/login?username=xxx&password=xxx"
$ curl -ik -XPOST "http://localhost:8080/user/login?username=xxx&password=xxx"
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Set-Cookie: JSESSIONID=D5240B930D22093D35DFC10A5860CD14; Path=/; HttpOnly
Content-Length: 62
Date: Thu, 31 Mar 2022 15:29:22 GMT
login successfully, token:44a3e490-06ef-4b18-aec5-ad3e7511e3bd
curl -ik -XGET -H "Authorization: 44a3e490-06ef-4b18-aec5-ad3e7511e3bd" "http://localhost:8080/getUser"
HTTP/1.1 404
Set-Cookie: JSESSIONID=E701BA24983F2463C4FFD0AC7EEF72E4; Path=/; HttpOnly
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 31 Mar 2022 15:36:47 GMT
{"timestamp":"2022-03-31T15:36:47.504+00:00","status":404,"error":"Not Found","path":"/getUser"}
404
呢?因为我们并没有定义/getUser
这个API,因为是演示token的校验机制,所以可以看到请求的校验也是通过了的curl -ik -XGET "http://localhost:8080/getUser"
HTTP/1.1 401
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 35
Date: Thu, 31 Mar 2022 15:41:00 GMT
401 token is invalid:token is empty
Spring Security功能很强大,对安全认证这一块提供了非常丰富的支持,篇幅有限,只是演示了非常基础的功能。如果大家在进行前后端分离的开发时,后端也想集成Spring Security,相信通过这篇文章的介绍,可以帮助大家解决一定的困惑。
本人对Spring Security的研究非常深入,几乎翻看了底层所有的源码,熟悉Spring Security运行的机制、原理,如果大家在使用Spring Security的过程中遇到什么难题,欢迎进一步沟通、交流。