SpringCloud+saToken实现登录及权限认证
文章目录
1.为什么要用sa-Token.
相比于我们常用的spring security框架,sa-Token更轻,更快,更优雅.使用过spring security的人肯定知道,spring security虽然灵活性很高,可以定制化各种复杂使用场景,但也因此导致框架很重,spring security使用的过滤器链每次请求认证也会花费大量性能,同时大部分功能都要自己手写完成.想比较之下,sa-Token更轻更快,开箱即用.比如登录认证时生成token的功能,spring security需要借助jwt写大量代码,而sa-Token只需要 StpUtil.login(Object id); 一个方法调用即可. 在 Sa-Token 中,大多数功能都可以一行代码解决
2. sa-Token功能
sa-Token可以满足我们大部分常规使用功能,可以解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题.
sa-Token权限认证实现方式和日常通用的设计思路,实现方式基本一致,只不过开箱即用,使用方便
3.springcloud集成sa-token
3.1 新建springcloud项目
我们需要至少有auth和gateway模块,auth作为用户登录认证模块,gateway里面需要实现网关统一鉴权功能,项目结构如下
3.2 auth模块功能实现
auth目录结构如下:
AuthController代码:
@RestController
@AllArgsConstructor
@RequestMapping("/auth/")
//@Api(value = "用户授权认证", tags = "授权接口")
public class AuthController {
@PostMapping("token")
public R<AuthInfo> doLogin(@ApiParam(value = "授权类型", required = true) @RequestParam(defaultValue = "PASSWORD", required = false) GrantType grantType,
@ApiParam(value = "租户ID", required = true) @RequestParam(defaultValue = "000000", required = false) String tenantId,
@ApiParam(value = "账号") @RequestParam(required = false) String account,
@ApiParam(value = "密码") @RequestParam(required = false) String password) {
UserParameter userParameter = UserParameter.builder()
.tenantId(tenantId)
.account(account)
.password(password)
.grantType(grantType)
.build();
//策略模式,兼容不同的登陆方式
IUserInfoGranter userInfoGranter = UserInfoGranterBuilder.getUserInfoGranter(grantType);
UserInfo userInfo = userInfoGranter.grant(userParameter);
if (userInfo == null || userInfo.getUser() == null || userInfo.getUser().getId() == null) {
return R.fail("用户名或密码错误");
}
return R.data(TokenGranter.createAuthInfo(userInfo));
}
// 查询登录状态,浏览器访问: http://localhost:8081/user/isLogin
@RequestMapping("isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
}
IUserInfoGranter
public interface IUserInfoGranter {
UserInfo grant(UserParameter userParameter);
}
PasswordUserInfoGranter(实际应用中密码需要加密处理)
@Component
@AllArgsConstructor
public class PasswordUserInfoGranter implements IUserInfoGranter {
@Override
public UserInfo grant(UserParameter userParameter) {
// String tenantId = userParameter.getTenantId();
String account = userParameter.getAccount();
String password = userParameter.getPassword();
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if ("admin".equals(account) && "admin".equals(password)) {
StpUtil.login(66666);
// 第2步,获取 Token 相关参数
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return UserInfo.builder()
.user((User.builder().id(10001L).build()))
.roles(Lists.newArrayList("1", "2", "3"))
.build();
}
return null;
}
}
PhoneNumUserInfoGranter
@Component
@AllArgsConstructor
public class PhoneNumUserInfoGranter implements IUserInfoGranter {
@Override
public UserInfo grant(UserParameter userParameter) {
//TODO
return null;
}
}
TokenGranter
public class TokenGranter {
/**
* 创建认证token
*
* @param userInfo 用户信息
* @return token
*/
public static AuthInfo createAuthInfo(UserInfo userInfo) {
User user = userInfo.getUser();
StpUtil.login(user.getId());
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
AuthInfo authInfo = new AuthInfo();
authInfo.setUserId(user.getId());
authInfo.setTenantId(StringUtils.toStr(user.getTenantId()));
authInfo.setOauthId(userInfo.getOauthId());
authInfo.setAccount(user.getAccount());
authInfo.setUserName(user.getRealName());
authInfo.setAuthority(userInfo.getRoles().toString().substring(1, userInfo.getRoles().toString().length() - 1));
authInfo.setAccessToken(tokenInfo.getTokenValue());
authInfo.setTokenType("satoken");
return authInfo;
}
}
UserInfoGranterBuilder
@AllArgsConstructor
public class UserInfoGranterBuilder {
private static final Map<GrantType, IUserInfoGranter> GRANTER_POOL = new HashMap<GrantType, IUserInfoGranter>();
static {
GRANTER_POOL.put(GrantType.PASSWORD, ApplicationContextHolder.getContext().getBean(PasswordUserInfoGranter.class));
GRANTER_POOL.put(GrantType.PHONENUM, ApplicationContextHolder.getContext().getBean(PhoneNumUserInfoGranter.class));
}
public static IUserInfoGranter getUserInfoGranter(GrantType grantType) {
IUserInfoGranter userInfoGranter = GRANTER_POOL.get(grantType);
if (userInfoGranter == null) {
throw new MSException("no grantType was found");
} else {
return userInfoGranter;
}
}
}
AuthInfo
@Data
public class AuthInfo {
@ApiModelProperty("令牌")
private String accessToken;
@ApiModelProperty("令牌类型")
private String tokenType;
@ApiModelProperty("用户ID")
@JsonSerialize(
using = ToStringSerializer.class
)
private Long userId;
@ApiModelProperty("租户ID")
private String tenantId;
@ApiModelProperty("第三方系统ID")
private String oauthId;
@ApiModelProperty("头像")
private String avatar = "https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png";
@ApiModelProperty("角色名")
private String authority;
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("账号名")
private String account;
}
User
@Data
@Builder
public class User {
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
@ApiModelProperty(value = "主键")
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private Long tenantId;
/**
* 编号
*/
private String code;
/**
* 账号
*/
private String account;
/**
* 密码
*/
private String password;
/**
* 昵称
*/
private String name;
/**
* 真名
*/
private String realName;
/**
* 头像
*/
private String avatar;
/**
* 邮箱
*/
private String email;
/**
* 手机
*/
private String phone;
/**
* 生日
*/
private Date birthday;
/**
* 性别
*/
private Integer sex;
/**
* 角色id
*/
private String roleId;
/**
* 部门id
*/
private String deptId;
/**
* 部门id
*/
private String postId;
}
UserInfo
@Data
@Builder
public class UserInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户基础信息
*/
@ApiModelProperty(value = "用户")
private User user;
/**
* 权限标识集合
*/
@ApiModelProperty(value = "权限集合")
private List<String> permissions;
/**
* 角色集合
*/
@ApiModelProperty(value = "角色集合")
private List<String> roles;
/**
* 第三方授权id
*/
@ApiModelProperty(value = "第三方授权id")
private String oauthId;
}
UserParameter
@Data
@Builder
public class UserParameter {
private String account;
private String password;
/**
* 租户ID
*/
private String tenantId;
/**
* 授权类型
*/
private GrantType grantType;
/**
* 刷新令牌
*/
private String refreshToken;
}
application.yml
server:
# 端口
port: 8100
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
is-share: true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
token-style: uuid
# 是否输出操作日志
is-log: true
#数据源配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://60.204.187.101:3306/blade?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&tinyInt1isBit=false&allowMultiQueries=true&serverTimezone=GMT%2B8
username: hetu2023
password: R9YGxWEl1m
data:
redis:
##redis 单机环境配置
host: 60.204.187.101
port: 3379
password:
database: 0
ssl:
enabled: false
##redis 集群环境配置
#cluster:
# nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
# commandTimeout: 5000
connect-timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
pom添加以下依赖,这里注意,我是用的时3版本的spring,如果使用的3以下的,把***boot3改成boot就好
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.37.0</version>
</dependency>
<!-- 需要引入Redis集成包,因为我们的网关和子服务主要通过Redis来同步数据 -->
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.37.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3.3 gateway模块功能实现
gateway目录结构如下:
SaTokenConfigure
@Configuration
public class SaTokenConfigure {
// 注册 Sa-Token全局过滤器
@Bean
public SaReactorFilter getSaReactorFilter() {
return new SaReactorFilter()
// 拦截地址
.addInclude("/**") /* 拦截全部path */
// 开放地址
.addExclude("/favicon.ico")
// 鉴权方法:每次访问进入
.setAuth(obj -> {
// 登录校验 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
SaRouter.match("/**", "/user/doLogin", r -> StpUtil.checkLogin());
// 权限认证 -- 不同模块, 校验不同权限
SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods"));
SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// 更多匹配 ... */
})
// 异常处理方法:每次setAuth函数出现异常时进入
.setError(e -> {
return SaResult.error(e.getMessage());
});
}
}
StpInterfaceImpl
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
application.yml
server:
port: 8080
spring:
application:
name: cloud-gateway-service
cloud:
gateway:
routes:
#访问http://localhost:8080/guonei就可以转发到http://news.baidu.com/guonei
- id: payment_routes2
uri: http://news.baidu.com/
predicates:
- Path=/guonei/**
- id: auth
uri: http://localhost:8100
predicates:
- Path=/auth/**
#开启自动定位功能 结合nacos注册中心使用
# discovery:
# locator:
# enabled: true
data:
redis:
##redis 单机环境配置
host: 60.204.187.101
port: 3379
password:
database: 0
ssl:
enabled: false
##redis 集群环境配置
#cluster:
# nodes: 127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003
# commandTimeout: 5000
connect-timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
pom中加入以下依赖,注意springcloud gateway使用的是Reactor 模型框架 ,需要引得依赖和mvc的不一样
<!-- Sa-Token 权限认证(Reactor响应式集成),在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-reactor-spring-boot3-starter</artifactId>
<version>1.37.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.37.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
3.4 测试
输入正确账号密码后:
请求无权限的路由后: