- 用户可以注册新帐户,或使用用户名和密码登录。
- 根据用户的权限,我们授权用户访问资源
今日内容介绍,大约花费40分钟
图片
1.Spring Boot 注册和登录with JWT 身份验证流程
下图显示了我们如何实现用户注册、用户登录和授权流程的流程。
图片
如果客户端访问受保护的资源,则必须将合法的 JWT 添加到 HTTP 授权标头中。
Spring Boot中使用Spring Security
您可以通过下图概述我们的Spring Boot项目:
图片
Spring Security介绍:
- WebSecurityConfig: spring Security 配置类,用于配置 Spring Security 的行为和规则。它为受保护的资源配置 cors、csrf、会话管理、规则。我们还可以扩展和自定义包含以下元素的默认配置。这个类通常扩展自 WebSecurityConfigurerAdapter
注意:WebSecurityConfigurerAdapter 从 SpringBoot 2.7.0 开始被弃用)
- UserDetailsService: UserDetailsService 是 Spring Security 中的一个接口,用于从数据源加载用户的详细信息,并返回一个 UserDetails 对象。UserDetails 包含有关用户的各种信息,例如用户名、密码、权限等。
- UserDetails: 实体类对象,包含构建 Authentication 对象所需的信息(例如:用户名、密码、权限)。
- UsernamePasswordAuthenticationToken: 从登录请求中获取 {username, password}, AuthenticationManager 将使用它来验证登录帐户。
- AuthenticationManager: 有一个DaoAuthenticationProvider (借助 UserDetailsService & PasswordEncoder )来验证 UsernamePasswordAuthenticationToken 对象。如果成功, AuthenticationManager 则返回完全填充的 Authentication 对象(包括授予的权限)。
- OncePerRequestFilter: 对 API 的每个请求进行一次执行。它提供了一种 doFilterInternal() 方法,我们将实现解析和验证JWT,加载用户详细信息(使用),检查授权(使用 UserDetailsService UsernamePasswordAuthenticationToken )。
- AuthenticationEntryPoint: 捕获身份验证错误。
2.项目准备
下图是Spring Boot项目的文件夹和文件结构:
图片
图片
- UserDetailsServiceImpl 实现 UserDetailsService
- AuthEntryPointJwt 实现 AuthenticationEntryPoint
- AuthTokenFilter 延伸 OncePerRequestFilter
- JwtUtils 提供用于生成、解析和验证 JWT 的方法
- 还有 application.yml,用于配置 Spring Datasource、Mybatis-Plus和项目属性(例如 JWT Secret 字符串或 Token 过期时间)
2.1. 创建表
根据Sql创建表,表间关系如下:
图片
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for pe_permission
-- ----------------------------
DROP TABLE IF EXISTS `pe_permission`;
CREATE TABLE `pe_permission` (
`id` varchar(40) NOT NULL COMMENT '主键',
`name` varchar(255) DEFAULT NULL COMMENT '权限名称',
`code` varchar(20) DEFAULT NULL,
`description` text COMMENT '权限描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of pe_permission
-- ----------------------------
INSERT INTO `pe_permission` VALUES ('1', '添加用户', 'user-add', null);
INSERT INTO `pe_permission` VALUES ('2', '查询用户', 'user-find', null);
INSERT INTO `pe_permission` VALUES ('3', '更新用户', 'user-update', null);
INSERT INTO `pe_permission` VALUES ('4', '删除用户', 'user-delete', null);
-- ----------------------------
-- Table structure for pe_role
-- ----------------------------
DROP TABLE IF EXISTS `pe_role`;
CREATE TABLE `pe_role` (
`id` varchar(40) NOT NULL COMMENT '主键ID',
`name` varchar(40) DEFAULT NULL COMMENT '权限名称',
`description` varchar(255) DEFAULT NULL COMMENT '说明',
PRIMARY KEY (`id`),
UNIQUE KEY `UK_k3beff7qglfn58qsf2yvbg41i` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of pe_role
-- ----------------------------
INSERT INTO `pe_role` VALUES ('1', '系统管理员', '系统日常维护');
INSERT INTO `pe_role` VALUES ('2', '普通员工', '普通操作权限');
-- ----------------------------
-- Table structure for pe_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `pe_role_permission`;
CREATE TABLE `pe_role_permission` (
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`permission_id` varchar(40) NOT NULL COMMENT '权限ID',
PRIMARY KEY (`role_id`,`permission_id`),
KEY `FK74qx7rkbtq2wqms78gljv87a0` (`permission_id`),
KEY `FKee9dk0vg99shvsytflym6egxd` (`role_id`),
CONSTRAINT `fk-p-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`),
CONSTRAINT `fk-pid` FOREIGN KEY (`permission_id`) REFERENCES `pe_permission` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of pe_role_permission
-- ----------------------------
INSERT INTO `pe_role_permission` VALUES ('1', '1');
INSERT INTO `pe_role_permission` VALUES ('1', '2');
INSERT INTO `pe_role_permission` VALUES ('2', '2');
INSERT INTO `pe_role_permission` VALUES ('1', '3');
INSERT INTO `pe_role_permission` VALUES ('1', '4');
-- ----------------------------
-- Table structure for pe_user
-- ----------------------------
DROP TABLE IF EXISTS `pe_user`;
CREATE TABLE `pe_user` (
`id` varchar(40) NOT NULL COMMENT 'ID',
`username` varchar(255) NOT NULL COMMENT '用户名称',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of pe_user
-- ----------------------------
INSERT INTO `pe_user` VALUES ('1', 'zhangsan', '$2a$10$.fXoccJHJkb9KM1FYJd1Ve.2P0B0RgLvloBDPwGjRxcP2obt2NRkG');
INSERT INTO `pe_user` VALUES ('2', 'lisi', '$2a$10$.fXoccJHJkb9KM1FYJd1Ve.2P0B0RgLvloBDPwGjRxcP2obt2NRkG');
INSERT INTO `pe_user` VALUES ('3', 'wangwu', '$2a$10$.fXoccJHJkb9KM1FYJd1Ve.2P0B0RgLvloBDPwGjRxcP2obt2NRkG');
-- ----------------------------
-- Table structure for pe_user_role
-- ----------------------------
DROP TABLE IF EXISTS `pe_user_role`;
CREATE TABLE `pe_user_role` (
`role_id` varchar(40) NOT NULL COMMENT '角色ID',
`user_id` varchar(40) NOT NULL COMMENT '权限ID',
KEY `FK74qx7rkbtq2wqms78gljv87a1` (`role_id`),
KEY `FKee9dk0vg99shvsytflym6egx1` (`user_id`),
CONSTRAINT `fk-rid` FOREIGN KEY (`role_id`) REFERENCES `pe_role` (`id`),
CONSTRAINT `fk-uid` FOREIGN KEY (`user_id`) REFERENCES `pe_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of pe_user_role
-- ----------------------------
INSERT INTO `pe_user_role` VALUES ('1', '1');
2.2. 在pom.xml中添加依赖
4.0.0
spring-boot-starter-parent
org.springframework.boot
2.7.15
com.zbbmeta
spring-boot-backend-example
1.0-SNAPSHOT
8
8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
com.baomidou
mybatis-plus-boot-starter
3.5.3
mysql
mysql-connector-java
8.0.30
org.projectlombok
lombok
cn.hutool
hutool-all
5.8.20
org.springframework.boot
spring-boot-configuration-processor
org.springframework.boot
spring-boot-starter-security
io.jsonwebtoken
jjwt
0.9.1
2.3. 使用MyBatisX插件生成代码
安装MyBatisX插件,这里就不过多介绍如何安装插件了,和其他插件安装相同 表生成代码步骤如下:
- 选择表右键选择MybatisX-Generator
图片
- 选择代码生成位置
图片
- 选择生成代码的规则
图片
- 我们根据规则将以下表进行生成:
- pe_permission
- pe_role
- pe_role_permission
- pe_user
- pe_user_role
2.4. 创建UserDetailsService实现类
Spring Security 将加载用户详细信息以执行身份验证和授权。所以它有 UserDetailsService 我们需要实现的接口。
在com.zbbmeta.service.impl包下创建UserDetailsService实现类UserDetailsServiceImpl
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByName(username);
List roles = user.getRoles();
List authorities = new ArrayList<>();
for (Role role : roles) {
List permissions = role.getPermissions();
List collect = permissions.stream().map(x -> x.getCode()).distinct().collect(Collectors.toList());
authorities.addAll(collect);
}
List collect1 = authorities.stream().distinct().collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(collect1.toString()));
}
}
2.5. 在Mapper类中添加查询用户和查询权限方法
- 在UserMapper添加根据用户名查询用户方法
public interface UserMapper extends BaseMapper {
User findByUsername(String name);
}
- 在UserMapper.xml添加方法的实现
在User实体类中添加字段roles
@TableField(exist = false)
private List roles = new ArrayList<>();//用户与角色 多对多
图片
并且在resultMap添加一对多根据用户查询对应角色
id,username,password
- 在RoleMapper添加根据id查询角色
public interface RoleMapper extends BaseMapper {
List queryRoleListByUserId(@Param("id") Long id);
}
- 在RoleMapper.xml添加方法的实现 在Role实体类中添加字段permissions
@TableField(exist = false)
private List permissions = new ArrayList<>();//用户与角色 多对多
图片
并且在resultMap添加一对多根据角色查询对应权限
id,name,description
- 在PermissionMapper添加根据id查询权限
public interface PermissionMapper extends BaseMapper {
List queryPermissionList(@Param("id") Long id);
}
- 在PermissionMapper.xml添加方法的实现
id,name,code,
description
3. 配置 Spring Security
注意:不使用 WebSecurityConfigurerAdapter,因为WebSecurityConfigurerAdapter 从 Spring 2.7.0 中弃用
【步骤一】: 在application.yml 添加jwt配置
jwt:
config:
key: zbbmeta
ttl: 3600
【步骤一】: 创建JwtUtil工具类
在com.zbbmeta.util包下创建JwtUtil类
package com.zbbmeta.util;
import io.jsonwebtoken.*;
import lombok.Data;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
@Component
@ConfigurationProperties("jwt.config")
@Data
public class JwtUtil {
private String key;
private long ttl;
public String createJwt(String id, String subject, Map map){
long now = System.currentTimeMillis();
long exp = now+ttl*1000;
JwtBuilder jwtBuilder =null;
try {
jwtBuilder = Jwts.builder().setId(id)
.setSubject(subject)
.setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, key);
for (Map.Entry stringObjectEntry : map.entrySet()) {
jwtBuilder.claim(stringObjectEntry.getKey(), stringObjectEntry.getValue());
}
if (ttl > 0) {
jwtBuilder.setExpiration(new Date(exp));
}
}catch (Exception e){
System.err.println(e.getMessage());
}
return jwtBuilder.compact();
}
public Claims parseJWT(String token){
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token).getBody();
return claims;
}
}
【步骤二】:创建 WebSecurityConfig 类
WebSecurityConfig类 是我们安全认证的关键。它为受保护的资源配置 cors、csrf、会话管理、规则。
package com.zbbmeta.config;
import com.zbbmeta.filter.AuthTokenFilter;
import com.zbbmeta.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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
public class WebSecurityConfig {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authTokenFilter() {
return new AuthTokenFilter();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth
@Slf4j
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
log.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map body = new HashMap<>();
body.put("code", HttpServletResponse.SC_UNAUTHORIZED);
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
【步骤五】:创建Controller进行登录和注册
- /api/auth/signup: 注册用户
- 检查现有 username
- 新建 User
- 保存 User 到数据库
- /api/auth/signin:用户登录
package com.zbbmeta.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.zbbmeta.api.Result;
import com.zbbmeta.api.ResultCode;
import com.zbbmeta.dto.LoginDto;
import com.zbbmeta.dto.SignupDto;
import com.zbbmeta.entity.Permission;
import com.zbbmeta.entity.Role;
import com.zbbmeta.entity.User;
import com.zbbmeta.entity.UserRole;
import com.zbbmeta.service.RoleService;
import com.zbbmeta.service.UserRoleService;
import com.zbbmeta.service.UserService;
import com.zbbmeta.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
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.RestController;
import java.util.*;
import java.util.stream.Collectors;
@RequestMapping("/api/auth")
@RestController
public class AuthController {
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@Autowired
UserRoleService userRoleService;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/signin")
public Result authenticateUser(@RequestBody LoginDto loginDto) {
//根据用户名查找用户
User user = userService.findByName(loginDto.getUsername());
//不存在表示登录失败
if(Objects.isNull(user)){
return Result.FAIL(ResultCode.USERNOEXIT_ERROR);
}
//密码不同登录失败
if(!passwordEncoder.matches(loginDto.getPassword(),user.getPassword())){
return Result.FAIL(ResultCode.PASSWORD_ERROR);
}
List collect = user.getRoles().stream().map(x -> x.getName()).collect(Collectors.toList());
StringBuilder builder = new StringBuilder();
List roles = user.getRoles();
for (Role role : roles) {
List permissions = role.getPermissions();
for (Permission permission : permissions) {
builder.append(permission.getCode()).append(",");
}
}
Map map = new HashMap<>();
map.put("username",user.getUsername());
map.put("permission",builder);
String token = jwtUtil.createJwt(user.getId(), user.getUsername(), map);
return Result.SUCCESS(token);
}
@PostMapping("/signup")
public Result registerUser( @RequestBody SignupDto signupDto){
//根据用户名获取用户
User user = userService.findByName(signupDto.getUsername());
//用户不是null表示用户已经存在
if(Objects.nonNull(user)){
return Result.FAIL(ResultCode.USER_ERROR);
}
//添加用户
User user1 = new User();
user1.setUsername(signupDto.getUsername());
user1.setPassword(signupDto.getPassword());
user1.setPassword(passwordEncoder.encode(signupDto.getPassword()));
List strRoles = signupDto.getRole();
List roles = new ArrayList<>();
//如果没有用户角色,默认添加普通员工
if (strRoles == null) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Role::getName,"普通员工");
Role role = roleService.getOne(wrapper);
roles.add(role);
}else {
strRoles.forEach(role ->{
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Role::getName,role);
Role adminRole = roleService.getOne(wrapper);
roles.add(adminRole);
});
}
//添加用户信息
boolean save = userService.save(user1);
String id = user1.getId();
List userRolesList = new ArrayList<>();
for (Role role : roles) {
UserRole userRoles = new UserRole();
userRoles.setUserId(id);
userRoles.setRoleId(role.getId());
userRolesList.add(userRoles);
}
//添加用户和角色关系
userRoleService.saveBatch(userRolesList);
return Result.SUCCESS("注册成功!");
}
}
4.测试
- 使用PostMan进行用户注册
图片
注册后的pe_user表格数据如下所示:
图片
- 访问受保护的资源:GET /api/tutorials/published
图片
因为我们没有登录,所以受保护的资源不能访问
- 登录账号:POST /api/auth/signin
图片
- 复制Token:重新访问受保护资源
图片
今天,我们在Spring Boot示例中学到关于Spring Security和基于JWT令牌的身份验证的有趣知识。尽管我们写了很多代码,但我希望你能理解应用程序的整体架构,并轻松地将其应用到你的项目中。
## 代码地址
https://github.com/bangbangzhou/spring-boot-backend-example.git