您现在的位置是:首页 >技术教程 >SpringSecurity实现前后端分离认证授权网站首页技术教程
SpringSecurity实现前后端分离认证授权
1、什么是SpringSecurity?
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求
从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。
怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。
Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
认证(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
授权(Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
2、先浅浅的来一个SpringSecurity入门
先创建一个普通的SpringBoot工程
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>-->
</dependencies>
编写一个请求
package com.ly.security.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 21:55
* @tags 喜欢就去努力的争取
*/
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
}
结果:正常访问,这简单不过了
整合Security呢?我们把前面pom.xml中的依赖包放开,刷新一下maven重启项目
再来访问一下我们的接口:http://localhost:8080/login
可以发现Security给我们拦下来了,需要我们做认证:输入用户名user
,密码:控制台复制
登录进去后,就又可以访问我们的接口了
以上就是一个简单的SpringBoot整合SpringSecurity的demo;下面将堆SpringSecurity安全认证框架进行详细的拆解描述
3、登录认证流程
注意:虽然SpringSecurity帮我们做了很多事情,但是很多东西是需要我们根据各自的系统进行定制的,如下:
1.登录页面是需要我们自己定义的
2.用户名和密码也应该是我们数据的用户名和密码
3.token我们怎么生成
4.token我们怎么解析
5.用户授权
…
认证详细流程
4、具体的实现思路
认证
- 1.自定义登录接口:调用providerManager进行认证,认证通过把用户信息存入Redis【key:userid,value:用户信息】;使用jwt生成token
- 2.自定义UserDetailService:查询数据库
校验
- 1.自定义jwt校验的过滤器:请求头中获取token —> 解析token拿到用户的信息 —> 去redis中获取完整的用户信息 —> 把完整的用户信息存入SecurityContextHolder供后续的过滤器使用
5、准备工作
(1)改造我们前面创建的快速入门工程;修改pom.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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ly.security</groupId>
<artifactId>security_quick_start</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jjwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
</project>
(2) 统一响应枚举 ResponseEnum
package com.ly.security.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 23:48
* @tags 喜欢就去努力的争取
*/
@Getter
@AllArgsConstructor
@ToString
public enum ResponseEnum {
SUCCESS(20000, "成功"),
ERROR(-1, "服务器内部错误"),
//2xx 参数校验
PASSWORD_NULL_ERROR(204, "密码不能为空"),
LOGIN_PASSWORD_ERROR(209, "密码错误"),
LOGIN_LOKED_ERROR(210, "用户被锁定"),
LOGIN_AUTH_ERROR(211, "未登录"),
;
/**
* 状态码
*/
private Integer code;
/**
* 消息
*/
private String message;
}
(3) 统一结果集返回 R
package com.ly.security.common;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 23:46
* @tags 喜欢就去努力的争取
*/
@Data
public class R {
private Integer code;
private String message;
private Map<String, Object> data = new HashMap();
/**
* 构造器私有
*/
private R() {
}
/**
* 返回成功
*/
public static R ok() {
R r = new R();
r.setCode(ResponseEnum.SUCCESS.getCode());
r.setMessage(ResponseEnum.SUCCESS.getMessage());
return r;
}
/**
* 返回失败
*/
public static R error() {
R r = new R();
r.setCode(ResponseEnum.ERROR.getCode());
r.setMessage(ResponseEnum.ERROR.getMessage());
return r;
}
/**
* 设置特定结果
*/
public static R setResult(ResponseEnum responseEnum) {
R r = new R();
r.setCode(responseEnum.getCode());
r.setMessage(responseEnum.getMessage());
return r;
}
public R message(String message) {
this.setMessage(message);
return this;
}
public R code(Integer code) {
this.setCode(code);
return this;
}
public R data(String key, Object value) {
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map) {
this.setData(map);
return this;
}
}
(4)Jwt工具类
package com.ly.security.utils;
import com.ly.security.exception.BusinessException;
import io.jsonwebtoken.*;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.Base64Utils;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 23:53
* @tags 喜欢就去努力的争取
*/
public class JwtUtils {
private static long tokenExpiration = 24 * 60 * 60 * 1000;
private static String tokenSignKey = "gek^/)_!e_5[(Spa[rAdc9Roby?JJs";
private static Key getKeyInstance() {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = Base64Utils.encode(tokenSignKey.getBytes());
return new SecretKeySpec(bytes, signatureAlgorithm.getJcaName());
}
/**
* 创建token
*
* @param id
* @param username
* @return
*/
public static String createToken(Long id, String username) {
return Jwts.builder()
.setSubject("ly")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("id", id)
.claim("username", username)
.signWith(SignatureAlgorithm.HS512, getKeyInstance())
.compressWith(CompressionCodecs.GZIP)
.compact();
}
/**
* 创建token
*
* @param id
* @return
*/
public static String createToken(String id) {
return Jwts.builder()
.setSubject("ly")
.setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
.claim("id", id)
.signWith(SignatureAlgorithm.HS512, getKeyInstance())
.compressWith(CompressionCodecs.GZIP)
.compact();
}
/**
* 判断token是否有效
*
* @param token
* @return
*/
public static boolean checkToken(String token) {
if (StringUtils.isBlank(token)) {
return false;
}
try {
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public static Long getUserId(String token) {
Claims claims = getClaims(token);
Long id = (Long) claims.get("id");
return id;
}
public static String getUsername(String token) {
Claims claims = getClaims(token);
return (String) claims.get("username");
}
public static void removeToken(String token) {
//jwttoken无需删除,客户端扔掉即可。
}
/**
* 校验token并返回Claims
*
* @param token
* @return
*/
private static Claims getClaims(String token) {
if (StringUtils.isEmpty(token)) {
// LOGIN_AUTH_ERROR(-211, "未登录"),
throw new BusinessException("未登录");
}
try {
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(token);
return claimsJws.getBody();
} catch (Exception e) {
throw new BusinessException("未登录");
}
}
public static void main(String[] args) {
String token = createToken("ly");
System.out.println("token = " + token);
boolean flag = checkToken(token);
Claims claims = getClaims(token);
String subject = claims.getSubject();
System.out.println("subject = " + subject);
}
}
(5) redis工具类
package com.ly.security.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 23:54
* @tags 喜欢就去努力的争取
*/
@Component
public class RedisCache {
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @return 缓存的对象
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value);
return operation;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
* @return 缓存的对象
*/
public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
operation.set(key, value, timeout, timeUnit);
return operation;
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(String key) {
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public void deleteObject(String key) {
redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection
*/
public void deleteObject(Collection collection) {
redisTemplate.delete(collection);
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList) {
ListOperations listOperation = redisTemplate.opsForList();
if (null != dataList) {
int size = dataList.size();
for (int i = 0; i < size; i++) {
listOperation.leftPush(key, dataList.get(i));
}
}
return listOperation;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(String key) {
List<T> dataList = new ArrayList<T>();
ListOperations<String, T> listOperation = redisTemplate.opsForList();
Long size = listOperation.size(key);
for (int i = 0; i < size; i++) {
dataList.add(listOperation.index(key, i));
}
return dataList;
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(String key) {
Set<T> dataSet = new HashSet<T>();
BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
dataSet = operation.members();
return dataSet;
}
/**
* 缓存Map
*
* @param key
* @param dataMap
* @return
*/
public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap) {
HashOperations hashOperations = redisTemplate.opsForHash();
if (null != dataMap) {
for (Map.Entry<String, T> entry : dataMap.entrySet()) {
hashOperations.put(key, entry.getKey(), entry.getValue());
}
}
return hashOperations;
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(String key) {
Map<String, T> map = redisTemplate.opsForHash().entries(key);
return map;
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
}
(6) WebUtils工具类,针对前后端分离写回JSON数据
package com.ly.security.utils;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-07 0:14
* @tags 喜欢就去努力的争取
*/
public class WebUtils {
/**
* @param response 渲染对象
* @param str 带渲染字符串
* @return
*/
public static String renderString(HttpServletResponse response, String str) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().write(str);
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
}
}
(7)统一异常
package com.ly.security.exception;
import com.ly.security.common.ResponseEnum;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-06 23:58
* @tags 喜欢就去努力的争取
*/
@Data
@NoArgsConstructor
public class BusinessException extends RuntimeException {
/**
* 状态码
*/
private Integer code;
/**
* 错误消息
*/
private String message;
/**
*
* @param message 错误消息
*/
public BusinessException(String message) {
this.message = message;
}
/**
*
* @param message 错误消息
* @param code 错误码
*/
public BusinessException(String message, Integer code) {
this.message = message;
this.code = code;
}
/**
*
* @param message 错误消息
* @param code 错误码
* @param cause 原始异常对象
*/
public BusinessException(String message, Integer code, Throwable cause) {
super(cause);
this.message = message;
this.code = code;
}
/**
*
* @param resultCodeEnum 接收枚举类型
*/
public BusinessException(ResponseEnum resultCodeEnum) {
this.message = resultCodeEnum.getMessage();
this.code = resultCodeEnum.getCode();
}
/**
*
* @param resultCodeEnum 接收枚举类型
* @param cause 原始异常对象
*/
public BusinessException(ResponseEnum resultCodeEnum, Throwable cause) {
super(cause);
this.message = resultCodeEnum.getMessage();
this.code = resultCodeEnum.getCode();
}
}
(8)统一异常处理
package com.ly.security.exception;
import com.ly.security.common.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.ConversionNotSupportedException;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-07 0:28
* @tags 喜欢就去努力的争取
*/
@Slf4j
@Component
@RestControllerAdvice // 在controller层添加通知。如果使用@ControllerAdvice,则方法上需要添加@ResponseBody
public class UnifiedExceptionHandler {
@ExceptionHandler(value = AccessDeniedException.class)
public R handleAccessDeniedException(AccessDeniedException e) {
log.error(e.getMessage(), e);
return R.error().code(-1).message(e.getMessage());
}
/**
* 自定义异常处理
*
* @param e
* @return
*/
@ExceptionHandler(value = BusinessException.class)
public R handleBusinessException(BusinessException e) {
log.error(e.getMessage(), e);
return R.error().code(e.getCode()).message(e.getMessage());
}
/**
* Controller上一层相关异常
*/
@ExceptionHandler({
NoHandlerFoundException.class,
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
MissingPathVariableException.class,
MissingServletRequestParameterException.class,
TypeMismatchException.class,
HttpMessageNotReadableException.class,
HttpMessageNotWritableException.class,
MethodArgumentNotValidException.class,
HttpMediaTypeNotAcceptableException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
MissingServletRequestPartException.class,
AsyncRequestTimeoutException.class
})
public R handleServletException(Exception e) {
log.error(e.getMessage(), e);
//SERVLET_ERROR(-102, "servlet请求异常"),
return R.error().message("servlet请求异常").code(-102);
}
/**
* 未定义异常
* 当controller中抛出Exception,则捕获
*/
@ExceptionHandler(value = Exception.class)
public R handleException(Exception e) {
log.error(e.getMessage(), e);
return R.error();
}
}
(9)数据库表
/*
Navicat MySQL Data Transfer
Source Server : startqbb
Source Server Type : MySQL
Source Server Version : 80029
Source Host : localhost:3306
Source Schema : ly_auth
Target Server Type : MySQL
Target Server Version : 80029
File Encoding : 65001
Date: 07/04/2023 00:43:03
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '编号',
`parent_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '所属上级',
`name` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '名称',
`type` tinyint(0) NOT NULL DEFAULT 0 COMMENT '类型(0:目录,1:菜单,2:按钮)',
`path` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址',
`component` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标',
`sort_value` int(0) NULL DEFAULT NULL COMMENT '排序',
`status` tinyint(0) NULL DEFAULT NULL COMMENT '状态(0:禁止,1:正常)',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_parent_id`(`parent_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`role_name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '角色名称',
`role_code` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色编码',
`description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`role_id` bigint(0) NOT NULL DEFAULT 0,
`menu_id` bigint(0) NOT NULL DEFAULT 0,
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_role_id`(`role_id`) USING BTREE,
INDEX `idx_menu_id`(`menu_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 105 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色菜单' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '会员id',
`username` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '姓名',
`phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机',
`head_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像地址',
`dept_id` bigint(0) NULL DEFAULT NULL COMMENT '部门id',
`post_id` bigint(0) NULL DEFAULT NULL COMMENT '岗位id',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述',
`status` tinyint(0) NULL DEFAULT 1 COMMENT '状态(1:正常 0:停用)',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `idx_username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '角色id',
`user_id` bigint(0) NOT NULL DEFAULT 0 COMMENT '用户id',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`is_deleted` tinyint(0) NOT NULL DEFAULT 0 COMMENT '删除标记(0:可用 1:已删除)',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_role_id`(`role_id`) USING BTREE,
INDEX `idx_admin_id`(`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 27 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
(10) 实体类
SysMenu:菜单
package com.ly.security.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;
import java.util.List;
/**
* 菜单
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
@TableName("sys_menu")
public class SysMenu extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 所属上级
*/
@TableField("parent_id")
private Long parentId;
/**
* 名称
*/
@TableField("name")
private String name;
/**
* 类型(1:菜单,2:按钮)
*/
@TableField("type")
private Integer type;
/**
* 路由地址
*/
@TableField("path")
private String path;
/**
* 组件路径
*/
@TableField("component")
private String component;
/**
* 权限标识
*/
@TableField("perms")
private String perms;
/**
* 图标
*/
@TableField("icon")
private String icon;
/**
* 排序
*/
@TableField("sort_value")
private Integer sortValue;
/**
* 状态(0:禁止,1:正常)
*/
@TableField("status")
private Integer status;
/**
* 下级列表
*/
@TableField(exist = false)
private List<SysMenu> children;
/**
* 是否选中
*/
@TableField(exist = false)
private boolean isSelect;
}
SysRole:角色
package com.ly.security.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;
/**
* 角色
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
@TableName("sys_role")
public class SysRole extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 角色名
*/
@TableField("role_name")
private String roleName;
/**
* 角色编码
*/
@TableField("role_code")
private String roleCode;
/**
* 描述
*/
@TableField("description")
private String description;
}
SysRoleMenu:角色菜单
package com.ly.security.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;
/**
* 角色菜单
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
@TableName("sys_role_menu")
public class SysRoleMenu extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 角色id
*/
@TableField("role_id")
private String roleId;
/**
* 菜单id
*/
@TableField("menu_id")
private String menuId;
}
SysUser:用户
package com.ly.security.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;
import java.util.List;
/**
* 用户
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
@TableName("sys_user")
public class SysUser extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@TableField("username")
private String username;
/**
* 密码
*/
@TableField("password")
private String password;
/**
* 姓名
*/
@TableField("name")
private String name;
/**
* 手机
*/
@TableField("phone")
private String phone;
/**
* 头像地址
*/
@TableField("head_url")
private String headUrl;
/**
* 部门id
*/
@TableField("dept_id")
private Long deptId;
/**
* 岗位id
*/
@TableField("post_id")
private Long postId;
/**
* 描述
*/
@TableField("description")
private String description;
/**
* 状态(1:正常 0:停用)
*/
@TableField("status")
private Integer status;
@TableField(exist = false)
private List<SysRole> roleList;
/**
* 岗位
*/
@TableField(exist = false)
private String postName;
/**
* 部门
*/
@TableField(exist = false)
private String deptName;
}
SysUserRole:用户角色
package com.ly.security.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ly.security.domain.base.BaseEntity;
import lombok.Data;
/**
* 用户角色
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
@TableName("sys_user_role")
public class SysUserRole extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 角色id
*/
@TableField("role_id")
private String roleId;
/**
* 用户id
*/
@TableField("user_id")
private String userId;
}
baseEntity:通用属性
package com.ly.security.domain.base;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-05 22:14
* @tags 喜欢就去努力的争取
*/
@Data
public class BaseEntity implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@TableLogic //逻辑删除 默认效果 0 没有删除 1 已经删除
@TableField("is_deleted")
private Integer isDeleted;
@TableField(exist = false)
private Map<String, Object> param = new HashMap<>();
}
(11) mybatis-plus自动填充
package com.ly.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-01 23:04
* @tags 喜欢就去努力的争取
*/
@Component
public class CommonMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
(12) 断言类Assert
package com.ly.security.common;
import com.ly.security.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.function.Supplier;
/**
* @author l&y (个人博客:https://www.cnblogs.com/qbbit)
* @date 2022-10-16 12:43
* @tags 我爱的人在很远的地方,我必须更加努力
*/
@Slf4j
public abstract class Assert {
/**
* 断言对象不为空
* 如果对象obj为空,则抛出异常
*
* @param obj 待判断对象
*/
public static void notNull(Object obj, ResponseEnum responseEnum) {
if (obj == null) {
log.info("obj is null...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言对象为空
* 如果对象obj不为空,则抛出异常
*
* @param object
* @param responseEnum
*/
public static void isNull(Object object, ResponseEnum responseEnum) {
if (object != null) {
log.info("obj is not null......");
throw new BusinessException(responseEnum);
}
}
/**
* 断言表达式为真
* 如果不为真,则抛出异常
*
* @param expression 是否成功
*/
public static void isTrue(boolean expression, ResponseEnum responseEnum) {
if (!expression) {
log.info("fail...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言表达式为假
* 如果不为真,则抛出异常
*
* @param expression 是否成功
*/
public static void isFalse(boolean expression, ResponseEnum responseEnum) {
if (expression) {
log.info("fail...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言两个对象不相等
* 如果相等,则抛出异常
*
* @param m1
* @param m2
* @param responseEnum
*/
public static void notEquals(Object m1, Object m2, ResponseEnum responseEnum) {
if (m1.equals(m2)) {
log.info("equals...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言两个对象相等
* 如果不相等,则抛出异常
*
* @param m1
* @param m2
* @param responseEnum
*/
public static void equals(Object m1, Object m2, ResponseEnum responseEnum) {
if (!m1.equals(m2)) {
log.info("not equals...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言参数不为空
* 如果为空,则抛出异常
*
* @param s
* @param responseEnum
*/
public static void notEmpty(String s, ResponseEnum responseEnum) {
if (StringUtils.isEmpty(s)) {
log.info("is empty...............");
throw new BusinessException(responseEnum);
}
}
/**
* 断言参数不为空
* 如果为空,则抛出异常
*
* @param list
* @param responseEnum
*/
public static void notEmpty(List<Long> list, ResponseEnum responseEnum) {
if (list == null || list.isEmpty()) {
log.info("is empty...............");
throw new BusinessException(responseEnum);
}
}
public static void custom(Supplier<Boolean> supplier, ResponseEnum responseEnum) {
if (supplier.get()) {
log.info("custom method error...............");
throw new BusinessException(responseEnum);
}
}
}
6、代码实现
(1)根据用户名从数据库中查询用户以及用户的权限信息
package com.ly.security.auth;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.domain.SysUser;
import com.ly.security.service.SysUserService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-07 22:30
* @tags 喜欢就去努力的争取
*/
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
/**
* 根据用户名查询用户和用户的权限信息
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 查询用户信息
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(StringUtils.isNotBlank(username), SysUser::getUsername, username));
Assert.notNull(sysUser, ResponseEnum.USER_NOT_EXIST);
// TODO 获取用户授权信息
// 封装为UserDetails对象返回
return new LoginUser(sysUser);
}
}
注意:我们的在这个loadUsername()方法,底层会由AuthenticationManager认证管理器进行自动调用
(2)把查询出来的用户信息封装和权限信息封装成UserDetails对象
我们创建一个LoginUser类是吸纳UserDetails接口
package com.ly.security.auth;
import com.ly.security.domain.SysUser;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-07 22:53
* @tags 喜欢就去努力的争取
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private SysUser sysUser;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return sysUser.getPassword();
}
@Override
public String getUsername() {
return sysUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return Boolean.TRUE;
}
@Override
public boolean isAccountNonLocked() {
return Boolean.TRUE;
}
@Override
public boolean isCredentialsNonExpired() {
return Boolean.TRUE;
}
@Override
public boolean isEnabled() {
return Boolean.TRUE;
}
}
(3)自定义密码加密方式
package com.ly.security.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* security配置类
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-09 15:07
* @tags 喜欢就去努力的争取
*/
@Configuration
@EnableWebSecurity
public class LySecurityConfig {
/**
* 密码加密器,会把客户端传来的密码进行加密,然后跟数据库中的密码做对比,要求数据库中的密码也是加密过的
* 如果没有该加密器,spring security会报出异常:There is no PasswordEncoder mapped for the id "null"
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
(4)编写一个login登录接口
package com.ly.security.controller;
import com.ly.security.common.R;
import com.ly.security.domain.vo.SysUserVo;
import com.ly.security.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import static com.ly.security.common.LyConstant.LOGIN_MSG;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-09 15:33
* @tags 喜欢就去努力的争取
*/
@RestController
public class LoginController {
@Autowired
private SysUserService sysUserService;
@PostMapping("/user/login")
public R login(@RequestBody SysUserVo sysUserVo){
sysUserService.login(sysUserVo);
return R.ok().message(LOGIN_MSG);
}
}
(5)SysUserServiceImpl实现类
package com.ly.security.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ly.security.auth.LoginUser;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.domain.SysUser;
import com.ly.security.domain.vo.SysUserVo;
import com.ly.security.mapper.SysUserMapper;
import com.ly.security.service.SysUserService;
import com.ly.security.utils.JwtUtils;
import com.ly.security.utils.RedisCache;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import static com.ly.security.common.LyConstant.LOGIN_USER_CACHE_KEY;
/**
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-03-10 20:15
* @tags 喜欢就去努力的争取
*/
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
private final AuthenticationManager authenticationManager;
private final RedisCache redisCache;
public SysUserServiceImpl(AuthenticationManager authenticationManager, RedisCache redisCache) {
this.authenticationManager = authenticationManager;
this.redisCache = redisCache;
}
@Override
public String login(SysUserVo sysUserVo) {
// 使用AuthenticationManager.authenticate进行用户认证 ---> 会去调用我们的UserDetailServiceImpl的loadUserByUsername进行认证
Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(sysUserVo.getUsername(), sysUserVo.getPassword()));
// 如果认证没通过,给出提示
Assert.notNull(authenticate, ResponseEnum.LOGIN_ERROR);
// 认证通过了,使用用户信息生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
Long userId = loginUser.getSysUser().getId();
String token = JwtUtils.createToken(userId.toString());
// 把用户信息存入redis
redisCache.setCacheObject(LOGIN_USER_CACHE_KEY + userId, token, 1, TimeUnit.DAYS);
// 返回token
return token;
}
}
(6) Security核心配置类
package com.ly.security.auth;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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;
/**
* security配置类
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-09 15:07
* @tags 喜欢就去努力的争取
*/
@Configuration
@EnableWebSecurity
public class LySecurityConfig {
/**
* 密码加密器,会把客户端传来的密码进行加密,然后跟数据库中的密码做对比,要求数据库中的密码也是加密过的
* 如果没有该加密器,spring security会报出异常:There is no PasswordEncoder mapped for the id "null"
* 拓展一下,这里也可以自定义密码加密方式,只需要自定义一个xxxPasswordEncoder 实现 PasswordEncoder 接口
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*
* @param authConfig
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
/**
* Security核心配置
*
* @param security
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity security) throws Exception {
return security
// 关闭csrf
.csrf().disable()
// 基于 token,不需要 session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 登录接口肯定是不需要认证的
.antMatchers("/user/login").anonymous()
// 这里意思是其它所有接口需要认证才能访问
.anyRequest().authenticated()
.and().build();
}
}
(7)配置文件
server:
port: 9001
spring:
application:
name: ly-security
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ly_auth?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
# 开启循环依赖
main:
allow-circular-references: true
# 配置日期格式化
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
redis:
host: 你的IP
port: 6379
timeout: 30000
lettuce:
pool:
max-active: 8
max-idle: 8
max-wait: -1
min-idle: 0
(8) 测试
7、token认证过滤器
(1) 自定义token认真过滤器
package com.ly.security.filter;
import com.ly.security.auth.LoginUser;
import com.ly.security.common.Assert;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.JwtUtils;
import com.ly.security.utils.RedisCache;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import static com.ly.security.common.LyConstant.LOGIN_USER_CACHE_KEY;
import static com.ly.security.common.LyConstant.TOKEN_KEY;
/**
* 自定义token过滤器
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-11 23:25
* @tags 喜欢就去努力的争取
*/
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
private RedisCache redisCache;
public JwtAuthenticationTokenFilter(RedisCache redisCache) {
this.redisCache = redisCache;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取token
String token = request.getHeader(TOKEN_KEY);
if (StringUtils.isBlank(token)) {
// token为空直接放行
filterChain.doFilter(request, response);
return;
}
// 解析token
Long userId = JwtUtils.getUserId(token);
// 拿着userId去Redis中获取用户信息
String cacheKey = LOGIN_USER_CACHE_KEY + userId;
LoginUser userInfo = redisCache.getCacheObject(cacheKey);
Assert.notNull(userInfo, ResponseEnum.LOGIN_AUTH_ERROR);
// 把用户信息存入SecurityContextHolder
// TODO 权限信息还未封装进去
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userInfo, null, null);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);
}
}
(2)配置token认证过滤器
......
.addFilterBefore(new JwtAuthenticationTokenFilter(redisCache), UsernamePasswordAuthenticationFilter.class)
......
8、退出登录
删除redis中的用户信息即可
@Override
public void logout() {
// 获取SecurityContextHolder中的用户信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 删除redis中的用户信息
Long id = loginUser.getSysUser().getId();
String cacheKey = LOGIN_USER_CACHE_KEY + id;
redisCache.deleteObject(cacheKey);
}
9、授权
我们需要查询出指定用户的权限
SELECT
DISTINCT sm.perms
FROM
sys_menu sm
LEFT JOIN sys_role_menu srm ON sm.id = srm.menu_id
LEFT JOIN sys_role sr ON sr.id = srm.role_id
LEFT JOIN sys_user_role sur ON sr.id = sur.role_id
LEFT JOIN sys_user su ON su.id = sur.user_id
WHERE
su.id = 1
AND sm.type = 2
AND su.`status` = 1
AND su.is_deleted = 0;
测试权限控制
UserDetailServiceImpl
List<String> permissions = sysMenuService.getPermissionsByUserId(sysUser.getId());
SysMenuServiceImpl
@Override
public List<String> getPermissionsByUserId(Long userId) {
Assert.notNull(userId, ResponseEnum.USER_NOT_EXIST);
QueryWrapper<SysMenu> wrapper = new QueryWrapper<>();
wrapper.eq("sur.user_id", userId)
.eq("sm.`status`", USER_STATUS)
.eq("sm.type", ENUM_TYPE)
.eq("srm.is_deleted", IS_DELETED_ZERO)
.eq("sm.is_deleted", IS_DELETED_ZERO)
.eq("sm.is_deleted", IS_DELETED_ZERO);
return sysMenuMapper.getPermissionsByUserId(wrapper);
}
SysMenuMapper
@Select("SELECT DISTINCT sm.perms FROM sys_menu sm INNER JOIN sys_role_menu srm ON sm.id = srm.menu_id INNER JOIN sys_user_role sur ON sur.role_id = srm.role_id ${ew.customSqlSegment}")
List<String> getPermissionsByUserId(@Param(Constants.WRAPPER) Wrapper<SysMenu> wrapper);
10、认证失败处理器
package com.ly.security.handler;
import com.ly.security.common.R;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.WebUtils;
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;
/**
* 认证失败处理器
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-13 23:58
* @tags 喜欢就去努力的争取
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
R r = R.setResult(ResponseEnum.UNAUTHORIZED);
WebUtils.renderString(response, r.toString());
}
}
别忘了在Security核心配置类配置一下
11、授权失败处理器
package com.ly.security.handler;
import com.ly.security.common.R;
import com.ly.security.common.ResponseEnum;
import com.ly.security.utils.WebUtils;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 授权失败处理器
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-14 0:03
* @tags 喜欢就去努力的争取
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
R r = R.setResult(ResponseEnum.FORBIDDEN);
WebUtils.renderString(response, r.toString());
}
}
别忘了在Security核心配置类配置一下
12、跨域配置
package com.ly.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 跨域配置
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-14 0:03
* @tags 喜欢就去努力的争取
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允诈跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
别忘了在Security核心配置类配置一下
13、hasAuthority()的原理
@PreAuthorize("hasAuthority('admin')")
:hasAuthority()中的我们在方法加的这段注解实际上是SPEL的一个表达式,Spring框架会替我们去解析它
所以我么点进方法看一看原理
- hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源.
- hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_这个前缀才可以。
- hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_这个前才可以。
14、自定义权限校验
前面我们大致的知道了权限校验的底层原理,现在我们就可以自定义一个权限校验的方法
package com.ly.security.expression;
import com.ly.security.auth.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 自定义权限控制
*
* @author ly (个人博客:https://www.cnblogs.com/qbbit)
* @date 2023-04-15 20:57
* @tags 喜欢就去努力的争取
*/
@Component("ex")
public class LyExpression {
public boolean hasAuthority(String authority) {
// 获取当前用户的权限列表
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
// 判断权限
return permissions.contains(authority);
}
}
使用SPEL调用我们自定义的权限校验方法
15、基于配置的方式实现权限控制
修改Security核心配置类
16、CSRF
- SpringSecurity去防止CSRF攻击的方式就是通过csrf token。后端会生成一个csrf token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
- 我们可以发现CSRF攻击依靠的cookie中所洪带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
最后回顾一下整个流程图
推荐观看Bilibili:三更大佬的视频