基于SpringBoot+SpringSecurity+JW

环境:Java8+SpringBoot 2.3.8+Spring Security 5.3.6 + java-jwt 3.12.0

主要是利用过滤器拦截(Filter)

实现了连接数据库,注册登录用户,使用Spring Security结合JWT实现安全认证。

前后端分离,使用json提交数据

Spring Security

Spring Security是一个功能强大、高度可定制的,并且专注于向Java应用提供**身份验证(authentication )授权(authorization)**的框架。

authentication与authorization

authentication主要是确定你是谁,而authorization是可以赋予你访问资源,做某些事的权利。

JWT(JSON Web Token)

JWT主要是用来确定用户的身份信息。在用户第一次携带用户名和密码访问服务器时,服务器签发一个token给用户。接下来用户携带着这个token访问服务器,就不再需要用户密码再次登录。

具体可参考该博客
什么是 JWT – JSON WEB TOKEN

表数据结构

本次用例采用MongoDB,这里的数据库实现的并不是很重要,大家可以随意:)

tb_user

tb_role

tb_resource

tb_role_res

SpringBoot

新建一个SpringBoot项目

本项目使用了lombok插件

@Data主要用于生成gettersetter

@AllArgsConstructor生成全参构造函数

@NoArgsConstructor生成无参构造函数

@Document(value = "tb_user")关联MongoDB的表

@Id声明表的主键

  • 如果你不使用lombok插件,按照往常写法即可
  • 与表的关联等操作,按照对应数据库的方法操作

有关的Json操作使用了fastjson

maven

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>

pom.xml

完整的pom.xml文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

entity

UserDo.java

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
java复制代码package com.example.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Document(value = "tb_user")
public class UserDo {

@Id
private String id; //Id
private String username; //用户名
private String password; //密码
private String role_name; //角色名

public UserDo(String username, String password, String role_name){
this.username = username;
this.password = password;
this.role_name = role_name;
}
}

RoleDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role")
public class RoleDo {

@Id
private Integer id; //Id
private String name; //角色名
}

ResourceDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_resource")
public class ResourceDo {

@Id
private Integer id; //Id
private String name; //资源名
private String desc; //描述
}

RoleResourceDo.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.example.demo.entity;

import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@Document(value = "tb_role_res")
public class RoleResourceDo {

@Id
private String id; //Id
private String role_name; //角色名
private String res_name; //资源名

}

dao

UserDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.example.demo.dao;

import com.example.demo.entity.UserDo;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UserDao extends MongoRepository<UserDo,String> {

/**
* 根据用户名查询用户
* @param username 用户名
* @return {@code UserDo} 用户对象
*/
UserDo getUserDoByUsername(String username);
}

service

UserService.java

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
java复制代码package com.example.demo.service;

import com.example.demo.entity.UserDo;

import java.util.List;

public interface UserService {

/**
* 根据用户名获取用户
* @param username 用户名
* @return 用户存在时,返回{@code UserDo}用户对象,
* 否则返回{@code null}
*/
UserDo getUserByUsername(String username);

/**
* 根据用户名获取用户角色
* @param username 用户名
* @return {@code String[]} 角色字符串数组
*/
String[] getRolesByUser(String username);

/**
* 添加一个新用户
* @param username 用户名
* @param password 密码
* @return 创建成功返回{@code UserDo}
*/
UserDo addUser(String username, String password);

/**
* 获取全部用户列表
* @return {@code List<UserDo>} 用户列表
*/
List<UserDo> getUserList();

/**
* 根据Header中的token获取用户信息
* @return {@code UserDo} 用户信息
*/
UserDo getUserByToken();
}

impl

UserServiceImpl.java

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
java复制代码package com.example.demo.service.impl;

import com.example.demo.dao.UserDao;
import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserDao userDao;

@Override
public UserDo getUserByUsername(String username) {
UserDo userDo = userDao.getUserDoByUsername(username);
if(userDo==null) return null;
return userDo;
}

@Override
public String[] getRolesByUser(String username) {
UserDo userDo = getUserByUsername(username);
String role_tmp = userDo.getRole_name();
String[] roles = role_tmp.split(",");
return roles;
}

@Override
public UserDo addUser(String username, String password) {
//使用Spring Security提供的BCryptPasswordEncoder加密用户密码存入数据库
//默认新加入的用户角色为user
return userDao.save(new UserDo(username, new BCryptPasswordEncoder().encode(password),"user"));
}

@Override
public List<UserDo> getUserList() {
return userDao.findAll();
}

@Override
public UserDo getUserByToken() {
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();
return userDao.getUserDoByUsername(user.getUsername());
}
}

controller

UserController.java

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
java复制代码package com.example.demo.controller;

import com.alibaba.fastjson.JSONObject;
import com.example.demo.common.CodeMsg;
import com.example.demo.common.Result;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {

@Autowired
private UserService userService;

/**
* 获取全部用户信息列表
* @return
*/
@RequestMapping("/admin/getUserList")
@ResponseBody
public Result getUserList(){
return new Result(CodeMsg.SUCCESS,userService.getUserList());
}

/**
* 根据Header中的token获取用户信息
* @return
*/
@RequestMapping("/user/info")
@ResponseBody
public Result getUserInfo(){
return new Result(CodeMsg.SUCCESS,userService.getUserByToken());
}

/**
* 根据用户名和密码创建新用户
* @param jsonObject username,password
* @return
*/
@PostMapping("/register")
@ResponseBody
public Result register(@RequestBody JSONObject jsonObject){
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");
return new Result(CodeMsg.REGISTER_SUCCESS,userService.addUser(username,password));
}
}

Spring Security

Spring Security是通过一系列的Filter(过滤器)来实现它的功能的

Filter的顺序与作用

以下是默认的Filter顺序

图片来源

在debug模式下可以看到过滤器执行的顺序

其中3,4是我自定义的过滤器,以下几种方法是用来添加过滤器的

1
2
3
4
5
6
java复制代码public HttpSecurity addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);	   //在某个过滤器后添加一个过滤器
public HttpSecurity addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter); //在某个过滤器前添加一个过滤器
//添加一个过滤器,该过滤器必须继承filters中的过滤器,也就是该过滤器的自定义过滤器
public HttpSecurity addFilter(Filter filter)
//在某一个过滤器的同一位置添加一个过滤器,但该过滤器不会覆盖原有的过滤器;并且这两个过滤器的执行顺序是不确定的
public HttpSecurity addFilterAt(Filter filter, Class<? extends Filter> atFilter);

filters

  • ChannelProcessingFilter
  • SecurityContextPersistenceFilter
  • LogoutFilter
  • X509AuthenticationFilter
  • AbstractPreAuthenticatedProcessingFilter
  • CasAuthenticationFilter
  • UsernamePasswordAuthenticationFilter
  • OpenIDAuthenticationFilter
  • DefaultLoginPageGeneratingFilter
  • DefaultLogoutPageGeneratingFilter
  • ConcurrentSessionFilter
  • DigestAuthenticationFilter
  • BearerTokenAuthenticationFilter
  • BasicAuthenticationFilter
  • RequestCacheAwareFilter
  • SecurityContextHolderAwareRequestFilter
  • JaasApiIntegrationFilter
  • RememberMeAuthenticationFilter
  • AnonymousAuthenticationFilter
  • SessionManagementFilter
  • ExceptionTranslationFilter
  • FilterSecurityInterceptor
  • SwitchUserFilter

Spring Security Web 5.1.2 源码解析 – 安全相关Filter清单

SpringSecurity过滤器顺序

认证流程

图片来源

图片来源

以上两张图很清晰地展现了Spring Security的主要认证流程。

在接收到请求后,先经过一系列的过滤器。

当被UsernamePasswordAuthenticationFilter拦截到后,调用其attemptAuthentication(request,response)方法,获取到username和password后,封装成UsernamePasswordAuthenticationToken

1
java复制代码UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

再让AuthenticationManager调用authenticate(authentication)方法进行验证。

1
java复制代码return this.getAuthenticationManager().authenticate(authenticationToken);

AuthenticationManager的默认实现是ProviderManager,其内部维护着一个AuthenticationProvider的列表。这个列表中存放的就是就是各种认证方式。当调用ProviderManagerauthenticate(authentication)方法时,会遍历该列表。

当认证成功时,会返回一个经过身份验证的对象Authentication,并且不会执行后面的认证方法。若全部验证失败,则会抛出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
26
27
28
java复制代码//部分ProviderManager的authenticate(Authentication authentication)方法源代码
for (AuthenticationProvider provider : getProviders()) { //遍历该列表
if (!provider.supports(toTest)) {
continue;
}

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

try {
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;
}
}

AuthenticationProvider的一个实现AbstractUserDetailsAuthenticationProvider会响应传过来的UsernamePasswordAuthenticationToken身份验证请求。

AbstractUserDetailsAuthenticationProvider调用authenticate(authentication)方法,根据传过来的Authentication获取用户名后,从缓存或者调用retrieveUser(username, authentication)方法。该方法由AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider实现,主要是调用了UserDetailsServiceloadUserByUsername(username)方法加载用户信息。我们一般会重写该方法,从数据库中取出用户信息。

在获取了正确的用户信息UserDetails和根据前面传数据过来后封装的UsernamePasswordAuthenticationToken后,调用DaoAuthenticationProvider实现的additionalAuthenticationChecks(userDetails,authentication)比较两者的密码是否一致完成验证。

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
java复制代码//retrieveUser方法源码
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
//此处可以看出调用了UserDetailsService().loadUserByUsername(username)
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);
}
}

如图是AuthenticationManagerAuthenticationProvider等几个类的关系

Json提交数据登录

因为Spring Security是默认通过form表单提交数据进行登录验证的。所以要通过URL访问后台并提交Json数据,需要修改Spring Security的配置。

在查阅资料的过程中,我发现在改写登录身份验证时,有的是从UsernamePasswordAuthenticationFilter继承,有的是从AbstractAuthenticationProcessingFilter继承。

UsernamePasswordAuthenticationFilter和AbstractAuthenticationProcessingFilter的区别

默认是使用UsernamePasswordAuthenticationFilter来拦截表单登录请求的,而UsernamePasswordAuthenticationFilter是从AbstractAuthenticationProcessingFilter继承而来的。

UsernamePasswordAuthenticationFilter

是用来处理表单登录的,默认登录URL为/login;需要提供两个参数:用户名和密码,也有默认的参数名,分别为usernamepassword。要修改它的认证方法,主要是通过重写 attemptAuthentication(request,response)方法。

1
java复制代码public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response);

若是使用表单登录,可以在继承WebSecurityConfigurerAdapter的类里修改

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置登录请求相关内容
http.formLogin()
.usernameParameter("user")
.passwordParameter("pswd")
.loginPage("/toLogin") //登录页
.loginProcessingUrl("/login"); //登录表单提交地址
}
}

若是使用Json传递数据登录,可以参考里的JwtLoginFilter

AbstractAuthenticationProcessingFilter

是基于浏览器HTTP的身份验证请求处理器

继承该类时,主要需要设置三个地方

  1. 设置authenticationManager属性,是用来处理身份认证请求的token
  2. 需要设置RequestMatcher设置拦截登录用的URL
  3. 实现attemptAuthentication()方法
AuthenticationSuccessHandler

当验证成功时,会调用该Handler,默认实现是SavedRequestAwareAuthenticationSuccessHandler。它将用户重定向到ExceptionTranslationFilter中设置的DefaultSavedRequest,否则会重定向到Web应用程序的根目录。

也可以在验证成功后,重写successfulAuthentication()方法,调用顺序是先执行successfulAuthentication(),再执行Handler。要注意的是,如果重写successfulAuthentication()时,没有调用chain.doFilter(request, response),则不会再调用Handler

AuthenticationFailureHandler

默认实现是SimpleUrlAuthenticationFailureHandler。它向客户端发送401错误代码,也可以配置失败的URL

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
java复制代码package com.example.demo.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.example.demo.util.ServletUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

/**
* 自定义的身份验证过滤器
*/
public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

public CustomAuthenticationFilter() {
//拦截 "/login" 的请求
super(new AntPathRequestMatcher("/login","POST"));
}

@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
// 因request.getParameter()不能获取到application/json中的数据
// 需要把用户名密码的读取逻辑修改为到流中读取request.getInputStream()
String body = ServletUtil.getBody(request);
JSONObject jsonObject = JSON.parseObject(body);
String username = jsonObject.getString("username");
String password = jsonObject.getString("password");


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

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

username = username.trim();

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password);

return this.getAuthenticationManager().authenticate(authenticationToken);
}
}

从数据库加载用户信息

上面提到过,DaoAuthenticationProviderretrieveUser(username, authentication)方法中调用了UserDetailsServiceloadUserByUsername(username)方法。所以我们继承UserDetailsService接口,实现该方法。

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
java复制代码package com.example.demo.service.impl;

import com.example.demo.entity.UserDo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Arrays;

/**
* 从数据库中加载用户信息
*/
@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {

@Autowired
private UserService userService;

/**
*
* @param username 用户名
* @return {@code UserDetails}
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDo userDo = userService.getUserByUsername(username);
if(userDo==null){
throw new UsernameNotFoundException("用户不存在");
}
// 查询成功后,用户存在,需要匹配用户密码是否正确
// 匹配密码是由 Spring Security 内部逻辑自动完成
//用户登录成功后,查询用户的权限集合。
String[] roles = userService.getRolesByUser(username);

String[] authorities = new String[roles.length];
for(int i = 0; i < roles.length; i++){
authorities[i] = "ROLE_" + roles[i];
}

System.out.println("用户" + userDo.getUsername() + "的权限集合是:" + Arrays.toString(authorities));

org.springframework.security.core.userdetails.User result =
new org.springframework.security.core.userdetails.User(username,userDo.getPassword(), AuthorityUtils.createAuthorityList(authorities));

return result;
}
}

JWT

JWT官网里面有各种语言的JWT实现,这里选择的是auth0的

maven

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.12.0</version>
</dependency>

JWT工具类

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
java复制代码package com.example.demo.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Date;
import java.util.function.Function;

/**
* jwt 工具类
*/
public class JwtTokenUtil {

public static final int JWT_TOKEN_VALIDITY = 30; //Token有效期,单位:分钟

public static final String SECRET = "Secret"; //私钥

/**
* 根据token获取用户名
* @param token
* @return {@code String} 用户名
*/
public static String getAudienceByToken(String token){
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
}catch (JWTDecodeException e){
e.printStackTrace();
throw new JWTDecodeException("jwt token解码失败");
}
return audience;
}

/**
* 根据token和实体名获取自定义实体
* @param token
* @param name 实体名
* @return {@code Claim}
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}

/**
* 根据用户名和私钥生成token
* @param username 用户名
* @return {@code String} token
*/
public static String generateToken(String username){
String token = null;

Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,JWT_TOKEN_VALIDITY);
Date expiresDate = nowTime.getTime();
try {
token = JWT.create()
.withAudience(username) //签发对象
.withIssuedAt(new Date()) //发行时间
.withExpiresAt(expiresDate)//有效时间
.sign(Algorithm.HMAC256(username+SECRET)); //加密算法

}catch (JWTCreationException exception){
exception.printStackTrace();
}
return token;
}

/**
* 根据token和用户名验证token是否正确
* @param token
* @param username 用户名
* @return {@code true} token正确
* {@code false} token错误
*/
public static Boolean validateToken(String token, String username){
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(username+SECRET))
.withAudience(username)
.build();
DecodedJWT jwt = verifier.verify(token);
return true;
}catch (TokenExpiredException e){
e.printStackTrace();
return false;
}catch (JWTVerificationException e){
e.printStackTrace();
return false;
}
}
}

添加JWT Token

在通过身份验证后,我们为利用用户的用户名和私钥生成一个Jwt token返回给前台。

新建一个JsonLoginSuccessHandler类来处理该逻辑

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
java复制代码package com.example.demo.handler;

import com.example.demo.util.JwtTokenUtil;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

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

/**
* 身份验证后,在header中添加jwt token
*/
public class JsonLoginSuccessHandler implements AuthenticationSuccessHandler {


@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

//添加token
String token = JwtTokenUtil.generateToken(((UserDetails)authentication.getPrincipal()).getUsername());
response.addHeader("Authorization",token);
}
}

配置Filter

配置一个登录失败的处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package com.example.demo.handler;

import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;

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

/**
* 登录失败处理器
* 回复401
*/
public class HttpStatusLoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}

配置Filter

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
java复制代码package com.example.demo.config;

import com.example.demo.filter.CustomAuthenticationFilter;
import com.example.demo.handler.HttpStatusLoginFailureHandler;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;

/**
* 配置CustomAuthenticationFilter
*/
public class JsonLoginConfig<T extends JsonLoginConfig<T, B>, B extends HttpSecurityBuilder<B>> extends AbstractHttpConfigurer<T, B> {

private CustomAuthenticationFilter authFilter;

public JsonLoginConfig(){
this.authFilter = new CustomAuthenticationFilter();
}

@Override
public void configure(B builder) throws Exception {
//设置Filter使用的AuthenticationManager,这里取公共的
authFilter.setAuthenticationManager(builder.getSharedObject(AuthenticationManager.class));
//设置失败的Handler
authFilter.setAuthenticationFailureHandler(new HttpStatusLoginFailureHandler());
//不将认证后的context放入session
authFilter.setSessionAuthenticationStrategy(new NullAuthenticatedSessionStrategy());

CustomAuthenticationFilter authenticationFilter = postProcess(authFilter);
//指定filter的位置
builder.addFilterAfter(authenticationFilter, LogoutFilter.class);
}

//设置成功的Handler,这个handler定义成Bean,所以从外面set进来
public JsonLoginConfig<T,B> loginSuccessHandler(AuthenticationSuccessHandler authenticationSuccessHandler){
authFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
return this;
}
}

验证Jwt 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
java复制代码package com.example.demo.filter;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.example.demo.service.impl.JwtUserDetailsServiceImpl;
import com.example.demo.util.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
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;

/**
* 每个需要验证的请求都验证token是否正确
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
private JwtUserDetailsServiceImpl jwtUserDetailsServiceImpl;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("JwtAuthorizationFilter执行...");
System.out.println("验证Token...");
UsernamePasswordAuthenticationToken authentication = getAuthentication(request);
if(authentication == null){
System.out.println("authentication null");
filterChain.doFilter(request,response);
return;
}

SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request,response);
}

/**
* 根据request中的token,验证后获取UsernamePasswordAuthenticationToken
* @param request
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){

String username = null;
String jwtToken = null;

jwtToken = request.getHeader("Authorization");

if(jwtToken != null){
try {
username = JwtTokenUtil.getAudienceByToken(jwtToken);
}catch (IllegalArgumentException e){
e.printStackTrace();
System.out.println("不能获取token或token不正确");
}catch (TokenExpiredException e){
e.printStackTrace();
System.out.println("token过期");
}
}

//获取token后验证
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
System.out.println("username:"+username);

UserDetails userDetails = jwtUserDetailsServiceImpl.loadUserByUsername(username);
if(JwtTokenUtil.validateToken(jwtToken,username)){ //验证token
//创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken= new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return usernamePasswordAuthenticationToken;
}
}

return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码package com.example.demo.config;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

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

/**
* 拒绝每个未经身份验证(token)的请求并发送错误代码401
*/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized");
System.out.println("JWT Unauthorized...");
}
}

Security配置

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
java复制代码package com.example.demo.config;

import com.example.demo.filter.JwtAuthenticationFilter;
import com.example.demo.handler.JsonLoginSuccessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;

@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);

//禁用form登录
http.formLogin().disable();


// 配置权限
http.authorizeRequests()
.antMatchers("/login","/register").permitAll()
// 基于角色的权限管理
.antMatchers("/admin/**","/user/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated(); //任意的请求,都必须认证后才能访问

// 添加过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

http.apply(new JsonLoginConfig<>()).loginSuccessHandler(jsonLoginSuccessHandler());

http.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint);

//使用无状态session,session不会储存用户状态
http.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);


//关闭CSRF安全协议
//关闭是为了保证完整流程的可用
http.csrf().disable();
}

// 注入密码编码器对象
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}

@Bean
protected JsonLoginSuccessHandler jsonLoginSuccessHandler(){
return new JsonLoginSuccessHandler();
}
}

通用类

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
java复制代码package com.example.demo.common;

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
public enum CodeMsg {

REGISTER_SUCCESS(2000,"注册成功"),
REGISTER_FAILURE(2001,"注册失败"),

LOGIN_SUCCESS(2002,"登录成功"),
LOGIN_FAILURE(2003,"登录失败"),

SUCCESS(2004,"获取数据成功"),
FAILURE(2005,"获取数据失败")


;

private int code;
private String message;

public int getCode() {
return code;
}

public void setCode(int code) {
this.code = code;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.example.demo.common;

import lombok.Data;

@Data
public class Result {

private int code;
private String message;
private Object entity;

public Result(){}
public Result(CodeMsg codeMsg, Object entity){
this.code=codeMsg.getCode();
this.message=codeMsg.getMessage();
this.entity=entity;
}

}
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
java复制代码package com.example.demo.util;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;

public class ServletUtil {

/**
* 获取请求中的body
* @param request
* @return
*/
public static String getBody(HttpServletRequest request){
StringBuilder stringBuilder = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try{
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine())!=null){
stringBuilder.append(line);
}
}catch (IOException e){
e.printStackTrace();
}finally {
if(inputStream != null){
try {
inputStream.close();
}catch (IOException e){
e.printStackTrace();
}
}
if(reader != null){
try{
reader.close();
}catch (IOException e){
e.printStackTrace();
}
}
}
return stringBuilder.toString();
}
}

测试

使用Postman进行测试

使用管理员的账号密码进行登录,可以看到返回的header中带有Authorization

下一次请求带上该token,成功获取数据

相同的操作,更换成普通用户登录。

获取token后访问只有管理员能访问的接口,可以看到返回了403没有权限

如果不带token访问,会返回401没有授权

小结

本文主要描述了使用如何使用Spring Security和JWT结合Springboot进行登录验证。实现这个功能,还有多种配置方式。可以使用Form表单登录,前后端结合在一起。还有更多个性化的配置,如不使用UsernamePasswordAuthenticationToken,新建一个Token类;还有使用多种验证方式,邮箱登录等。

参考

Spring Security 案例实现和执行流程剖析

Spring Security做JWT认证和授权

自定义SpringSecurity认证方式

SpringSecurity 核心组件介绍 + 认证流程 +内置拦截器顺序

Spring-Security权限框架

尚硅谷SpringSecurity框架教程

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%