Spring Boot Security + JWT Token 的簡單應(yīng)用
今天主要介紹以下內(nèi)容:
- 用戶可以注冊新帳戶,或使用用戶名和密碼登錄。
- 根據(jù)用戶的權(quán)限,我們授權(quán)用戶訪問資源
今日內(nèi)容介紹,大約花費40分鐘
圖片
1.Spring Boot 注冊和登錄with JWT 身份驗證流程
下圖顯示了我們?nèi)绾螌崿F(xiàn)用戶注冊、用戶登錄和授權(quán)流程的流程。
圖片
如果客戶端訪問受保護的資源,則必須將合法的 JWT 添加到 HTTP 授權(quán)標(biāo)頭中。
Spring Boot中使用Spring Security
您可以通過下圖概述我們的Spring Boot項目:
圖片
Spring Security介紹:
- WebSecurityConfig: spring Security 配置類,用于配置 Spring Security 的行為和規(guī)則。它為受保護的資源配置 cors、csrf、會話管理、規(guī)則。我們還可以擴展和自定義包含以下元素的默認(rèn)配置。這個類通常擴展自 WebSecurityConfigurerAdapter
注意:WebSecurityConfigurerAdapter 從 SpringBoot 2.7.0 開始被棄用)
- UserDetailsService: UserDetailsService 是 Spring Security 中的一個接口,用于從數(shù)據(jù)源加載用戶的詳細(xì)信息,并返回一個 UserDetails 對象。UserDetails 包含有關(guān)用戶的各種信息,例如用戶名、密碼、權(quán)限等。
- UserDetails: 實體類對象,包含構(gòu)建 Authentication 對象所需的信息(例如:用戶名、密碼、權(quán)限)。
- UsernamePasswordAuthenticationToken: 從登錄請求中獲取 {username, password}, AuthenticationManager 將使用它來驗證登錄帳戶。
- AuthenticationManager: 有一個DaoAuthenticationProvider (借助 UserDetailsService & PasswordEncoder )來驗證 UsernamePasswordAuthenticationToken 對象。如果成功, AuthenticationManager 則返回完全填充的 Authentication 對象(包括授予的權(quán)限)。
- OncePerRequestFilter: 對 API 的每個請求進行一次執(zhí)行。它提供了一種 doFilterInternal() 方法,我們將實現(xiàn)解析和驗證JWT,加載用戶詳細(xì)信息(使用),檢查授權(quán)(使用 UserDetailsService UsernamePasswordAuthenticationToken )。
- AuthenticationEntryPoint: 捕獲身份驗證錯誤。
2.項目準(zhǔn)備
下圖是Spring Boot項目的文件夾和文件結(jié)構(gòu):
圖片
圖片
- UserDetailsServiceImpl 實現(xiàn) UserDetailsService
- AuthEntryPointJwt 實現(xiàn) AuthenticationEntryPoint
- AuthTokenFilter 延伸 OncePerRequestFilter
- JwtUtils 提供用于生成、解析和驗證 JWT 的方法
- 還有 application.yml,用于配置 Spring Datasource、Mybatis-Plus和項目屬性(例如 JWT Secret 字符串或 Token 過期時間)
2.1. 創(chuàng)建表
根據(jù)Sql創(chuàng)建表,表間關(guān)系如下:
圖片
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 '權(quán)限名稱',
`code` varchar(20) DEFAULT NULL,
`description` text COMMENT '權(quán)限描述',
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 '權(quán)限名稱',
`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', '系統(tǒng)管理員', '系統(tǒng)日常維護');
INSERT INTO `pe_role` VALUES ('2', '普通員工', '普通操作權(quán)限');
-- ----------------------------
-- 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 '角色I(xiàn)D',
`permission_id` varchar(40) NOT NULL COMMENT '權(quán)限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 '角色I(xiàn)D',
`user_id` varchar(40) NOT NULL COMMENT '權(quán)限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中添加依賴
<?xml versinotallow="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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.15</version>
</parent>
<groupId>com.zbbmeta</groupId>
<artifactId>spring-boot-backend-example</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-configuration-processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</project>
2.3. 使用MyBatisX插件生成代碼
安裝MyBatisX插件,這里就不過多介紹如何安裝插件了,和其他插件安裝相同 表生成代碼步驟如下:
- 選擇表右鍵選擇MybatisX-Generator
圖片
- 選擇代碼生成位置
圖片
- 選擇生成代碼的規(guī)則
圖片
- 我們根據(jù)規(guī)則將以下表進行生成:
- pe_permission
- pe_role
- pe_role_permission
- pe_user
- pe_user_role
2.4. 創(chuàng)建UserDetailsService實現(xiàn)類
Spring Security 將加載用戶詳細(xì)信息以執(zhí)行身份驗證和授權(quán)。所以它有 UserDetailsService 我們需要實現(xiàn)的接口。
在com.zbbmeta.service.impl包下創(chuàng)建UserDetailsService實現(xiàn)類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<Role> roles = user.getRoles();
List<String> authorities = new ArrayList<>();
for (Role role : roles) {
List<Permission> permissions = role.getPermissions();
List<String> collect = permissions.stream().map(x -> x.getCode()).distinct().collect(Collectors.toList());
authorities.addAll(collect);
}
List<String> 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類中添加查詢用戶和查詢權(quán)限方法
- 在UserMapper添加根據(jù)用戶名查詢用戶方法
public interface UserMapper extends BaseMapper<User> {
User findByUsername(String name);
}
- 在UserMapper.xml添加方法的實現(xiàn)
在User實體類中添加字段roles
@TableField(exist = false)
private List<Role> roles = new ArrayList<>();//用戶與角色 多對多
圖片
并且在resultMap添加一對多根據(jù)用戶查詢對應(yīng)角色
<?xml versinotallow="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zbbmeta.mapper.UserMapper">
<resultMap id="BaseResultMap" type="com.zbbmeta.entity.User">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<!--一對多映射用這個 ofTyp是一對多的集合的所存放的實體類 javaType實體類的屬性類型-->
<collection property="roles" ofType="com.zbbmeta.entity.Role"
select="com.zbbmeta.mapper.RoleMapper.queryRoleListByUserId" column="id">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
id,username,password
</sql>
<select id="findByUsername" resultMap="BaseResultMap">
select * from pe_user where username=#{name};
</select>
</mapper>
- 在RoleMapper添加根據(jù)id查詢角色
public interface RoleMapper extends BaseMapper<Role> {
List<Role> queryRoleListByUserId(@Param("id") Long id);
}
- 在RoleMapper.xml添加方法的實現(xiàn) 在Role實體類中添加字段permissions
@TableField(exist = false)
private List<Permission> permissions = new ArrayList<>();//用戶與角色 多對多
圖片
并且在resultMap添加一對多根據(jù)角色查詢對應(yīng)權(quán)限
<?xml versinotallow="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zbbmeta.mapper.RoleMapper">
<resultMap id="BaseResultMap" type="com.zbbmeta.entity.Role">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
<collection property="permissions" ofType="com.zbbmeta.entity.Permission"
select="com.zbbmeta.mapper.PermissionMapper.queryPermissionList" column="id">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="code" column="code" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<sql id="Base_Column_List">
id,name,description
</sql>
<select id="queryRoleListByUserId" resultMap="BaseResultMap">
select distinct pr.* from pe_user_role pur
inner join pe_role pr on pur.role_id =pr.id
where pur.user_id =#{id}
</select>
</mapper>
- 在PermissionMapper添加根據(jù)id查詢權(quán)限
public interface PermissionMapper extends BaseMapper<Permission> {
List<Permission> queryPermissionList(@Param("id") Long id);
}
- 在PermissionMapper.xml添加方法的實現(xiàn)
<?xml versinotallow="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zbbmeta.mapper.PermissionMapper">
<resultMap id="BaseResultMap" type="com.zbbmeta.entity.Permission">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="name" column="name" jdbcType="VARCHAR"/>
<result property="code" column="code" jdbcType="VARCHAR"/>
<result property="description" column="description" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List">
id,name,code,
description
</sql>
<select id="queryPermissionList" resultType="com.zbbmeta.entity.Permission">
select * from pe_permission pp
inner join pe_role_permission prp on pp.id = prp.permission_id
where prp.role_id=#{id}
</select>
</mapper>
3. 配置 Spring Security
注意:不使用 WebSecurityConfigurerAdapter,因為WebSecurityConfigurerAdapter 從 Spring 2.7.0 中棄用
【步驟一】: 在application.yml 添加jwt配置
jwt:
config:
key: zbbmeta
ttl: 3600
【步驟一】: 創(chuàng)建JwtUtil工具類
在com.zbbmeta.util包下創(chuàng)建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;
/**
* @author springboot葵花寶典
* @description: TODO
*/
@Component
@ConfigurationProperties("jwt.config")
@Data
public class JwtUtil {
private String key;
private long ttl;
public String createJwt(String id, String subject, Map<String,Object> 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<String, Object> 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;
}
}
【步驟二】:創(chuàng)建 WebSecurityConfig 類
WebSecurityConfig類 是我們安全認(rèn)證的關(guān)鍵。它為受保護的資源配置 cors、csrf、會話管理、規(guī)則。
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;
/**
* @author springboot葵花寶典
* @description: TODO
*/
@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;
}
/**
* 認(rèn)證
* @param http
* @return
* @throws Exception
*/
@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/**").permitAll()//不需要通過登錄驗證就可以被訪問的資源路徑
.anyRequest().authenticated();
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(authTokenFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
注: 同樣的在 Spring Security 5.8 后要把 antMatchers() 改為 requestMatchers()
- @EnableWebSecurity 允許 Spring 查找并自動將該類應(yīng)用于全局 Web 安全。
- AuthenticationManagerBuilder.userDetailsService() 方法進行配置 DaoAuthenticationProvider 。并且我們還需要一個 PasswordEncoder對密碼進行加密 .如果我們不指定,它將使用純文本。
【步驟三】:創(chuàng)建過濾器篩選請求
在 com.zbbmeta.filter包下創(chuàng)建AuthTokenFilter類,對每個請求進行過濾。 AuthTokenFilter 集成 OncePerRequestFilter 和覆蓋 doFilterInternal() 方法的類。
@Slf4j
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
//登錄或者注冊放行
if(requestURI.contains("/api/auth")){
filterChain.doFilter(request, response);
return ;
}
String authorization = request.getHeader("Authorization");
if(StrUtil.isNotBlank(authorization)){
String token = authorization.replace("Bearer ", "");
Claims claims = jwtUtil.parseJWT(token);
String username = claims.get("username", String.class);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
我們在里面 doFilterInternal() 做什么:
- 從 Authorization 標(biāo)頭獲取 JWT (通過刪除 Bearer 前綴)
- – 如果請求有 JWT ,請驗證它, 將username 從token中解析
【步驟四】:處理身份驗證異常
創(chuàng)建 AuthEntryPointJwt 實現(xiàn)接口的 AuthenticationEntryPoint 類。然后我們重寫該 commence() 方法。每當(dāng)未經(jīng)身份驗證的用戶請求受保護的 HTTP 資源并 AuthenticationException 拋出時,都會觸發(fā)此方法。
package com.zbbmeta.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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;
import java.util.HashMap;
import java.util.Map;
/**
* @author springboot葵花寶典
* @description: TODO
*/
@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<String, Object> 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);
}
}
【步驟五】:創(chuàng)建Controller進行登錄和注冊
- /api/auth/signup: 注冊用戶
- 檢查現(xiàn)有 username
- 新建 User
- 保存 User 到數(shù)據(jù)庫
- /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;
/**
* @author springboot葵花寶典
* @description: TODO
*/
@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) {
//根據(jù)用戶名查找用戶
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<String> collect = user.getRoles().stream().map(x -> x.getName()).collect(Collectors.toList());
StringBuilder builder = new StringBuilder();
List<Role> roles = user.getRoles();
for (Role role : roles) {
List<Permission> permissions = role.getPermissions();
for (Permission permission : permissions) {
builder.append(permission.getCode()).append(",");
}
}
Map<String, Object> 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);
}
/**
* 用戶注冊
* @param signupDto
* @return
*/
@PostMapping("/signup")
public Result registerUser( @RequestBody SignupDto signupDto){
//根據(jù)用戶名獲取用戶
User user = userService.findByName(signupDto.getUsername());
//用戶不是null表示用戶已經(jīng)存在
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<String> strRoles = signupDto.getRole();
List<Role> roles = new ArrayList<>();
//如果沒有用戶角色,默認(rèn)添加普通員工
if (strRoles == null) {
LambdaQueryWrapper<Role> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Role::getName,"普通員工");
Role role = roleService.getOne(wrapper);
roles.add(role);
}else {
strRoles.forEach(role ->{
LambdaQueryWrapper<Role> 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<UserRole> userRolesList = new ArrayList<>();
for (Role role : roles) {
UserRole userRoles = new UserRole();
userRoles.setUserId(id);
userRoles.setRoleId(role.getId());
userRolesList.add(userRoles);
}
//添加用戶和角色關(guān)系
userRoleService.saveBatch(userRolesList);
return Result.SUCCESS("注冊成功!");
}
}
4.測試
- 使用PostMan進行用戶注冊
圖片
注冊后的pe_user表格數(shù)據(jù)如下所示:
圖片
- 訪問受保護的資源:GET /api/tutorials/published
圖片
因為我們沒有登錄,所以受保護的資源不能訪問
- 登錄賬號:POST /api/auth/signin
圖片
- 復(fù)制Token:重新訪問受保護資源
圖片
今天,我們在Spring Boot示例中學(xué)到關(guān)于Spring Security和基于JWT令牌的身份驗證的有趣知識。盡管我們寫了很多代碼,但我希望你能理解應(yīng)用程序的整體架構(gòu),并輕松地將其應(yīng)用到你的項目中。
## 代碼地址
https://github.com/bangbangzhou/spring-boot-backend-example.git