身份验证又称“验证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。
身份验证的目的是确认当前所声称为某种身份的用户,确实是所声称的用户。在日常生活中,身份验证并不罕见;比如,通过检查对方的证件,我们一般可以确信对方的身份。
在互联网中身份验证极为重要,不论是web端还是移动端、小程序等,在与后台交互的过程中都是需要携带身份信息的,只有通过身份认证后,后台才会执行相关请求。
常见的认证模式 cookie模式所谓 cookie ,本质上是一个特殊的header参数。
cookie是一种客户端会话技术,将数据保存在客户端。一小段文本信息随着请求和响应,在客户端和服务器端之间来回传递。根据设定的时间来决定该段文本在客户端保存时长的这种工作模式。如果服务器创建cookie后,会以key=value的形式传递到客户端,并保存在客户端。一旦客户端有服务器发回的文本信息,那么当浏览器再次向服务器发起请求时,也会以key=value这样的形式将文本信息发送到服务器端。
cookie是一段不超过4KB的小型文本数据,由一个名称(Name)、一个值(Value)和其它几个用于控制cookie有效期、安全性、使用范围的可选属性组成。如下图所示:
cookie作用保持客户端和服务器之间的状态客户端存储用户的认证信息客户端存储简单数据 cookie特点
常规PC端鉴权方法,一般由cookie模式完成,而 cookie 有两个特性:
- 可由后端控制写入每次请求自动提交
这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)。
cookie安全cookie是可以被客户端修改的,所有它是有安全问题的,重要的隐私信息不能存储在cookie中,我们要慎用cookie。
注意事项:
cookie不能存储用户账号密码cookie需要设置超时时间cookie在账号退出时需要最好主动删除 前后台分离(无cookie模式)
而在app、小程序等前后台分离场景中,一般是没有 cookie 这一功能的。
这里我们就需要使用token来进行认证。
- 前端进行用户登录后,后台会返回一个token到前端前端在进行接口请求时,将token存放到header中,格式为:{tokenName: tokenValue}后台接收到token就会开始鉴权判断是否有权力进行该接口请求,无token的请求会被直接拒绝提示未登录
使用token模式进行交互,很容易实现SSO单点登录。
使用sa-token这里我们使用sa-token框架完成单体SpringBoot项目的权限认证功能,选择前后台分离模式,即使用token进行权限认证。前端可以是VUE、APP和小程序等客户端。
sa-token是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权 等一系列权限相关问题。简化了我们开发权限管理的业务逻辑。
sa-token
集成步骤 用户管理表这里我们使用RBAC模型,RBAC 是基于角色的访问控制(Role-based Access Control )在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
这里我除了用户、角色、权限还多增加了一个部门
用户表DROp TABLE IF EXISTS auth_user;
CREATE TABLE auth_user(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
updated_time timestamp COMMENT '修改时间' ,
username VARCHAR(255) COMMENT '用户名' ,
password VARCHAR(255) COMMENT '密码' ,
email VARCHAR(255) COMMENT '邮箱' ,
phone INTEGER COMMENT '手机号' ,
is_deleted VARCHAR(1) DEFAULT 0 COMMENT '是否删除;0:未删除,1:已删除' ,
is_enable VARCHAR(1) DEFAULT 0 COMMENT '是否启用;0:未启用,1:启用' ,
PRIMARY KEY (id)
) COMMENT = '用户信息';
角色表
DROP TABLE IF EXISTS auth_role;
CREATE TABLE auth_role(
id INT NOT NULL COMMENT '唯一标识' ,
created_time DATETIME DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '角色名称' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '角色';
权限表
DROP TABLE IF EXISTS auth_permit;
CREATE TABLE auth_permit(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '权限名称' ,
url VARCHAR(255) COMMENT '授权路径' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '权限';
部门表
DROP TABLE IF EXISTS auth_org;
CREATE TABLE auth_org(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '部门名称' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '部门机构';
用户角色表
DROP TABLE IF EXISTS auth_user_role;
CREATE TABLE auth_user_role(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
user_id INT COMMENT '用户id' ,
role_id INT COMMENT '角色id' ,
PRIMARY KEY (id)
) COMMENT = '用户角色表';
角色权限表
DROP TABLE IF EXISTS auth_role_permit;
CREATE TABLE auth_role_permit(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
role_id INT COMMENT '角色ID' ,
permit_id INT COMMENT '权限ID' ,
PRIMARY KEY (id)
) COMMENT = '角色权限表';
部门用户表
DROP TABLE IF EXISTS auth_org_user;
CREATE TABLE auth_org_user(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
org_id INT COMMENT '部门id' ,
user_id INT COMMENT '用户id' ,
PRIMARY KEY (id)
) COMMENT = '部门用户表';
以上7张表基本涵盖了常用的RBAC模式,如果需要可以根据具体的业务进行扩展
项目配置我这里使用的是SpringBoot项目,数据库是PostgreSQL(MySQL也可以),使用mybatis plus框架,数据库连接池为Druid。
默认各位已经熟悉SpringBoot项目的使用,这里只列出了关键配置,像实体类、controller层等都需自己去新建。
项目结构如图:
添加maven依赖sa-token依赖
cn.dev33
sa-token-spring-boot-starter
1.29.0
完整maven依赖
application.yml配置org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web com.alibaba fastjson 1.2.76 commons-logging commons-logging org.postgresql postgresql runtime com.alibaba druid-spring-boot-starter 1.1.23 com.baomidou mybatis-plus-boot-starter 3.4.0 com.zaxxer HikariCP com.github.pagehelper pagehelper-spring-boot-starter 1.3.0 cn.dev33 sa-token-spring-boot-starter 1.29.0 org.springframework.security spring-security-crypto 5.1.5.RELEASE
sa-token配置
# Sa-Token配置 sa-token: # token名称 (同时也是cookie名称) token-name: token # token有效期,单位s 默认30天, -1代表永不过期 timeout: 86400 # token临时有效期 (指定时间内无操作就视为token过期) 单位: 秒 activity-timeout: -1 # 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录) is-concurrent: true # 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token) is-share: true # token风格 token-style: uuid # 是否输出操作日志 is-log: false
完整配置
server:
port: 8081
spring:
datasource:
url: jdbc:postgresql://${base.config.db.hostname}:${base.config.db.port}/${base.config.db.db}?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: ${base.config.db.username}
password: ${base.config.db.password}
driver-class-name: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
# 配置初始化大小、最小、最大
initial-size: 5
minIdle: 10
max-active: 20
# 配置获取连接等待超时的时间(单位:毫秒)
max-wait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
time-between-eviction-runs-millis: 2000
# 配置一个连接在池中最小生存的时间,单位是毫秒
min-evictable-idle-time-millis: 600000
max-evictable-idle-time-millis: 900000
# 用来测试连接是否可用的SQL语句,默认值每种数据库都不相同,这是mysql
validationQuery: select 1
# 应用向连接池申请连接,并且testOnBorrow为false时,连接池将会判断连接是否处于空闲状态,如果是,则验证这条连接是否可用
testWhileIdle: true
# 如果为true,默认是false,应用向连接池申请连接时,连接池会判断这条连接是否是可用的
testOnBorrow: false
# 如果为true(默认false),当应用使用完连接,连接池回收连接的时候会判断该连接是否还可用
testOnReturn: false
# 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle
poolPreparedStatements: true
# 要启用PSCache,必须配置大于0,当大于0时, poolPreparedStatements自动触发修改为true,
# 在Druid中,不会存在Oracle下PSCache占用内存过多的问题,
# 可以把这个数值配置大一些,比如说100
maxOpenPreparedStatements: 20
# 连接池中的minIdle数量以内的连接,空闲时间超过minEvictableIdleTimeMillis,则会执行keepAlive操作
keepAlive: true
# Spring 监控,利用aop 对指定接口的执行时间,jdbc数进行记录
aop-patterns: "com.dzzh.big_screen.mapper.*"
########### 启用内置过滤器(第一个 stat必须,否则监控不到SQL)##########
filters: stat,wall,log4j2
# 自己配置监控统计拦截的filter
filter:
# 开启druidDatasource的状态监控
stat:
enabled: true
db-type: postgresql
# 开启慢sql监控,超过2s 就认为是慢sql,记录到日志中
log-slow-sql: true
slow-sql-millis: 2000
# 日志监控,使用slf4j 进行日志输出
slf4j:
enabled: true
statement-log-error-enabled: true
statement-create-after-log-enabled: false
statement-close-after-log-enabled: false
result-set-open-after-log-enabled: false
result-set-close-after-log-enabled: false
########## 配置WebStatFilter,用于采集web关联监控的数据 ##########
web-stat-filter:
enabled: true # 启动 StatFilter
url-pattern:
ResponseResult doLogin(String username, String password);
serviceImpl层
@Service
public class LoginServiceImpl implements LoginService {
@Resource
AuthUserMapper authUserMapper;
@Resource
BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public ResponseResult doLogin(String username, String password) {
QueryWrapper queryUser = new QueryWrapper<>();
queryUser.eq("username", username);
AuthUser authUser = authUserMapper.selectOne(queryUser);
if (authUser == null) {
return new ResponseResult(ResponseCode.FAILURE,"用户不存在!");
}
Integer id = authUser.getId();
String userPassword = authUser.getPassword();
if (!"1".equals(authUser.getIsEnable())) {
return new ResponseResult(ResponseCode.FAILURE, "用户未激活,请联系管理员!");
}
if (!bCryptPasswordEncoder.matches(password, userPassword)) {
return new ResponseResult(ResponseCode.FAILURE, "用户名密码不正确!");
}
// sa-token登录
StpUtil.login(id);
SaTokenInfo saTokenInfo = StpUtil.getTokenInfo();
String token = saTokenInfo.getTokenValue();
long tokenTimeout = saTokenInfo.getTokenTimeout();
authUser.setPassword("");
HashMap
返回的结构如图所示:
前台拿到token,就可以存起来了。
到这集成sa-token已经完成50%了,这里实现了认证,后面需要实现授权。
配置sa-token授权在RBAC模型中不同的用户拥有不同的角色,不同的角色拥有的权限是不一样的。接口拦截需要和权限进行绑定,拥有权限的用户可以访问对应的接口。为了实现这个效果,我们需要自定义实现拦截器。
配置拦截器继承WebMvcConfigurer实现自定义的拦截方法,设置指定的接口需要指定的权限,
SaTokenConfigure.java
import cn.dev33.satoken.interceptor.SaRouteInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册Sa-Token的注解拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
registry.addInterceptor(new SaRouteInterceptor((req, res, handler) -> {
// 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("
@Component
public class StpInterfaceImpl implements StpInterface {
@Resource
AuthUserMapper userMapper;
@Override
public List getPermissionList(Object loginId, String loginType) {
System.out.println();
List permission = userMapper.getPermitByUserId(Integer.valueOf(loginId.toString()));
System.out.println(permission);
return permission.stream().map(AuthPermit::getName).collect(Collectors.toList());
}
}
AuthUserMapper.java
List getPermitByUserId(Integer id);
AuthUserMapper.xml
配置全局异常
sa-token很贴心的为我们提供了全局异常处理示例
封装返回值AjaxJson.java
import java.io.Serializable;
import java.util.List;
public class AjaxJson implements Serializable{
// 序列化版本号
private static final long serialVersionUID = 1L;
// 成功状态码
public static final int CODE_SUCCESS = 200;
// 错误状态码
public static final int CODE_ERROR = 500;
// 警告状态码
public static final int CODE_WARNING = 501;
// 无权限状态码
public static final int CODE_NOT_JUR = 403;
// 未登录状态码
public static final int CODE_NOT_LOGIN = 401;
// 无效请求状态码
public static final int CODE_INVALID_REQUEST = 400;
// 状态码
public int code;
// 描述信息
public String msg;
// 携带对象
public Object data;
// 数据总数,用于分页
public Long dataCount;
public int getCode() {
return this.code;
}
public AjaxJson setMsg(String msg) {
this.msg = msg;
return this;
}
public String getMsg() {
return this.msg;
}
public AjaxJson setData(Object data) {
this.data = data;
return this;
}
@SuppressWarnings("unchecked")
public T getData(Class cs) {
return (T) data;
}
// ============================ 构建 ==================================
public AjaxJson(int code, String msg, Object data, Long dataCount) {
this.code = code;
this.msg = msg;
this.data = data;
this.dataCount = dataCount;
}
// 返回成功
public static AjaxJson getSuccess() {
return new AjaxJson(CODE_SUCCESS, "ok", null, null);
}
public static AjaxJson getSuccess(String msg) {
return new AjaxJson(CODE_SUCCESS, msg, null, null);
}
public static AjaxJson getSuccess(String msg, Object data) {
return new AjaxJson(CODE_SUCCESS, msg, data, null);
}
public static AjaxJson getSuccessData(Object data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
public static AjaxJson getSuccessArray(Object... data) {
return new AjaxJson(CODE_SUCCESS, "ok", data, null);
}
// 返回失败
public static AjaxJson getError() {
return new AjaxJson(CODE_ERROR, "error", null, null);
}
public static AjaxJson getError(String msg) {
return new AjaxJson(CODE_ERROR, msg, null, null);
}
// 返回警告
public static AjaxJson getWarning() {
return new AjaxJson(CODE_ERROR, "warning", null, null);
}
public static AjaxJson getWarning(String msg) {
return new AjaxJson(CODE_WARNING, msg, null, null);
}
// 返回未登录
public static AjaxJson getNotLogin() {
return new AjaxJson(CODE_NOT_LOGIN, "未登录,请登录后再次访问", null, null);
}
// 返回没有权限的
public static AjaxJson getNotJur(String msg) {
return new AjaxJson(CODE_NOT_JUR, msg, null, null);
}
// 返回一个自定义状态码的
public static AjaxJson get(int code, String msg){
return new AjaxJson(code, msg, null, null);
}
// 返回分页和数据的
public static AjaxJson getPageData(Long dataCount, Object data){
return new AjaxJson(CODE_SUCCESS, "ok", data, dataCount);
}
// 返回,根据受影响行数的(大于0=ok,小于0=error)
public static AjaxJson getByLine(int line){
if(line > 0){
return getSuccess("ok", line);
}
return getError("error").setData(line);
}
// 返回,根据布尔值来确定最终结果的 (true=ok,false=error)
public static AjaxJson getByBoolean(boolean b){
return b ? getSuccess("ok") : getError("error");
}
@SuppressWarnings("rawtypes")
@Override
public String toString() {
String data_string = null;
if(data == null){
} else if(data instanceof List){
data_string = "List(length=" + ((List)data).size() + ")";
} else {
data_string = data.toString();
}
return "{"
+ ""code": " + this.getCode()
+ ", "msg": "" + this.getMsg() + """
+ ", "data": " + data_string
+ ", "dataCount": " + dataCount
+ "}";
}
}
全局异常
GlobalException.java
import cn.dev33.satoken.exception.DisableLoginException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@ControllerAdvice
public class GlobalException {
// 全局异常拦截(拦截项目中的所有异常)
@ResponseBody
@ExceptionHandler
public AjaxJson handlerException(Exception e, HttpServletRequest request, HttpServletResponse response) {
// 打印堆栈,以供调试
System.out.println("全局异常---------------");
// System.out.println(e.getMessage());
// 不同异常返回不同状态码
AjaxJson aj = null;
// 如果是未登录异常
if (e instanceof NotLoginException) {
NotLoginException ee = (NotLoginException) e;
aj = AjaxJson.getNotLogin().setMsg(ee.getMessage());
}
// 如果是角色异常
else if (e instanceof NotRoleException) {
NotRoleException ee = (NotRoleException) e;
aj = AjaxJson.getNotJur("无此角色:" + ee.getRole());
}
// 如果是权限异常
else if (e instanceof NotPermissionException) {
NotPermissionException ee = (NotPermissionException) e;
aj = AjaxJson.getNotJur("无此权限:" + ee.getCode());
}
// 如果是被封禁异常
else if (e instanceof DisableLoginException) {
DisableLoginException ee = (DisableLoginException) e;
aj = AjaxJson.getNotJur("账号被封禁:" + ee.getDisableTime() + "秒后解封");
} else { // 普通异常, 输出:500 + 异常信息
aj = AjaxJson.getError(e.getMessage());
}
// 返回给前端
return aj;
}
}
权限认证测试
无token测试
这里模拟head没有token的情况(用户未登录),如图所示会提醒401
登录成功后无权限测试这里使用测试账号,角色为测试角色未分配select-user权限
这里提醒为无权限
登录后有权限测试这里使用管理员账号,角色为管理员,分配了select-user权限
可以看到有返回值了
结语sa-token真是利器,使用它可以为我们减少不少的开发过程,而且这个是国产开源项目,stars也不少,文档对国人很友好,支持国产开源!
这里我要说一下,部分内容参考我朋友的教程springboot集成 Sa-Token、mybatisplus实现RPAC_ct1104的博客-CSDN博客
后面有时间再说说sa-token在微服务中的使用!



