SpringBoot2.X实战Shiro(Token)登录和注册
源代码仓库:https://github.com/zhshuixian/learn-spring-boot-2
在上一节"Spring Security (Token)登录和注册"中,主要介绍了 Spring Boot 整合 Spring Security 实现 Token 的登录和认证,这一小节中,我们将实现 Spring Boot 整合 Shiro 实现 Token 的登录和认证。
1)Apache Shiro 简介
在前面介绍过,Java 开发常用的安全框架有 Spring Security 和 Apache Shiro,这里将简要介绍一下 Shiro,Shiro 是一个功能强大的开源安全框架:
Apache Shiro™**是一个功能强大且易于使用的 Java 安全框架,用于执行身份验证,授权,加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。
对于使用 Shiro,需要了解其三个核心概念:
- Subject:主题,是一个安全术语,表示"当前正在执行的用户"
- SecurityManager : 安全管理器,是 Shiro 的核心,提供各种安全管理的服务和管理所有的 Subject。
- Realm : Realm 是应用程序和安全数据之间的"桥梁"或者"连接器",当 Shiro 需要和安全数据(例如:用户账户信息)交互以实现身份验证(登录认证)和授权(访问控制),Shiro 会通过其配置的一个或者多个 Realm 实现。
扩展阅读:http://shiro.apache.org/architecture.html
2)Shiro 项目配置
新建一个项目,07-shiro,记得勾选 MySQL,MyBatis,Web 依赖。对于 Maven 项目,同样是在文章最后面给出对应的依赖配置。
Gradle 项目配置
implementation "org.springframework.boot:spring-boot-starter-web" implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2"
runtimeOnly "mysql:mysql-connector-java"
testImplementation "org.springframework.boot:spring-boot-starter-test"
// https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter
compile group: "org.apache.shiro", name: "shiro-spring-boot-web-starter", version: "1.5.2"
// https://github.com/jwtk/jjwt
compile "io.jsonwebtoken:jjwt-api:0.11.1"
runtime "io.jsonwebtoken:jjwt-impl:0.11.1",
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
//"org.bouncycastle:bcprov-jdk15on:1.60",
// or "io.jsonwebtoken:jjwt-gson:0.11.1" for gson
"io.jsonwebtoken:jjwt-jackson:0.11.1"
2.1)项目配置
配置 MySQL 数据库和 MyBatis 驼峰命名转换,application.properties
# 数据库 URL、用户名、密码、JDBC Driver更换数据库只需更改这些信息即可# MySQL 8 需要指定 serverTimezone 才能连接成功
spring.datasource.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.password=xiaoxian
spring.datasource.username=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# MyBatis 驼峰命名转换
mybatis.configuration.map-underscore-to-camel-case=true
添加 @MapperScan
@MapperScan("org.xian.security.mapper")public class SecurityApplication {}
3)开始使用 Shiro
项目的主要结构:
- controller 包:API 接口
- service 包:为 API 提供接口服务
- mapper 包:MyBatis Mapper 类
- entity 包:实体类
- Shiro 包:Token 拦截验证、Token 生成、Shiro 的 Realm 等配置
MyResponse :公共 Response 返回消息类:
public class MyResponse implements Serializable { private static final long serialVersionUID = -2L;
private String status;
private String message;
}
2.1)实体类 Entity 和 Mapper
这里的表结构和上一节一样, 用户表 sys_user
字段 类型 备注
user_id
bigint
自增主键
username
varchar(18)
用户名,非空唯一
password
varchar(128)
密码,非空
user_role
varchar(8)
用户角色(USER / ADMIN)
user_permission
varchar(36)
用户权限
这里用户角色有 USER / ADMIN ,对于一个用户可能有多个角色的情况暂不考虑。
Shiro 可以指定相应的权限控制,比如 Update 的权限,Create 的权限,使得可以更加细粒度的控制。这里的权限用英文分号 , 直接隔开,例如:update,create,delete ,表示该用户具有 Update 等三个权限。
密码使用 HASH 散列加密。
SQL
create table sys_user(
user_id bigint auto_increment,
username varchar(18) not null unique,
password varchar(128) not null,
user_role varchar(8) not null,
user_permission varchar(36) not null,
constraint sys_user_pk
primary key (user_id)
);
Entity 实体类:新建 package,名称为 entity 。在 entity 下新建一个 SysUser 类:
public class SysUser implements Serializable { private static final long serialVersionUID = 4522943071576672084L;
private Long userId;
private String username;
private String password;
private String userRole;
private String userPermission;
// 省略 getter setter constructor
}
Mapper 接口类:新建包 mapper,新建 SysUserMapper 类:
public interface SysUserMapper { /** 往 sys_user 插入一条记录
* @param sysUser 用户信息
*/
@Insert("Insert Into sys_user(username, password,user_role,user_permission) Values(#{username}, #{password},#{userRole},#{userPermission})")
@Options(useGeneratedKeys = true, keyProperty = "userId")
void insert(SysUser sysUser);
/** 根据用户 Username 查询用户信息
* @param username 用户名
* @return 用户信息
*/
@Select("Select user_id,username, password,user_role,user_permission From sys_user Where username=#{username}")
SysUser selectByUsername(String username);
}
2.2)Token 配置
首先实现 Token 生成和验证的功能:
- RSA 密钥公钥工具类
- Token 生成、验证工具类
在 shiro 包下新建 RsaUtils 类,RSA 的公钥和密钥的工具类。注意在 JDK 8 中,2048 位的密钥不受支持。代码参考 Spring Boot 2.X 实战--Spring Security (Token)登录和注册 的 2.2)Token 配置 的小节:
TokenUtils : 生成和验证 Token 的工具类。可选的 Token 主体部分是指在验证和授权的时候用不上这些信息,主要的代码和上一节差不多,主要是增加一个 Refresh 刷新 Token 的功能:
@Componentpublic class TokenUtils implements Serializable {
private static final long serialVersionUID = -3L;
/** Token 有效时长 多少秒 **/
private static final Long EXPIRATION = 2 * 60L;
/** 生成 Token 字符串 setAudience 接收者 setExpiration 过期时间 role 用户角色
* @param sysUser 用户信息
* @return 生成的Token字符串 or null
*/
public String createToken(SysUser sysUser) {
try {
// Token 的过期时间
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
// 生成 Token
String token = Jwts.builder()
// 设置 Token 签发者 可选
.setIssuer("SpringBoot")
// 根据用户名设置 Token 的接受者
.setAudience(sysUser.getUsername())
// 设置过期时间
.setExpiration(expirationDate)
// 设置 Token 生成时间 可选
.setIssuedAt(new Date())
// 通过 claim 方法设置一个 key = role,value = userRole 的值
.claim("role", sysUser.getUserRole())
// 用户角色
// 通过 claim 方法设置一个 key = permission,value = Permission 的值
.claim("permission", sysUser.getUserPermission())
// 设置加密密钥和加密算法,注意要用私钥加密且保证私钥不泄露
.signWith(RsaUtils.getPrivateKey(), SignatureAlgorithm.RS256)
.compact();
return String.format("Bearer %s", token);
} catch (Exception e) {
return null;
}
}
/** 验证 Token ,并获取到用户名和用户权限信息
* @param token Token 字符串
* @return sysUser 用户信息
*/
public SysUser validationToken(String token) {
try {
// 解密 Token,获取 Claims 主体
Claims claims = Jwts.parserBuilder()
// 设置公钥解密,以为私钥是保密的,因此 Token 只能是自己生成的,如此来验证 Token
.setSigningKey(RsaUtils.getPublicKey())
.build().parseClaimsJws(token).getBody();
assert claims != null;
SysUser sysUser = new SysUser();
// 获得用户信息
sysUser.setUsername(claims.getAudience());
sysUser.setUserRole(claims.get("role").toString());
sysUser.setUserPermission(claims.get("permission").toString());
return sysUser;
} catch (Exception e) {
return null;
}
}
}
Token 刷新部分在后面单独来讲。
2.3)Shiro 配置
实现 Shiro 的 Realm,拦截器等
- ShiroAuthToken :实现 AuthenticationToken 接口
- ShiroRealm :自定义 Realm,验证 Token 和从 Token 中取得用户角色和权限
- ShiroAuthFilter :拦截器,拦截所有请求,并验证 Token
- ShiroConfig :Shiro 配置,将 Realm、拦截器等配置到 SecurityManager 中
ShiroAuthToken :实现 AuthenticationToken 接口,作为 Token 传入到 Realm 的载体:
public class ShiroAuthToken implements AuthenticationToken { private String token;
public ShiroAuthToken(String token) { this.token = token; }
@Override
public Object getPrincipal() { return token; }
@Override
public Object getCredentials() { return token; }
}
ShiroRealm :从 ShiroAuthToken 取得 Token 并进行身份验证和角色权限配置。
@Servicepublic class ShiroRealm extends AuthorizingRealm {
@Resource
TokenUtils tokenUtils;
@Override
public boolean supports(AuthenticationToken authenticationToken) {
// 指定当前 authenticationToken 需要为 ShiroAuthToken 的实例
return authenticationToken instanceof ShiroAuthToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
ShiroAuthToken shiroAuthToken = (ShiroAuthToken) authenticationToken;
String token = (String) shiroAuthToken.getCredentials();
// 验证 Token
SysUser sysUser = tokenUtils.validationToken(token);
if (sysUser == null || sysUser.getUsername() == null || sysUser.getUserRole() == null) {
throw new AuthenticationException("Token 无效");
}
return new SimpleAuthenticationInfo(token,
token, "ShiroRealm");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 获取用户信息
SysUser sysUser = tokenUtils.validationToken(principals.toString());
// 创建一个授权对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 判断用户角色是否存在
if (!sysUser.getUserRole().isEmpty()) {
// 角色设置
info.addRole(sysUser.getUserRole());
}
if (!sysUser.getUserPermission().isEmpty()) {
// 进行权限设置,根据 , 分割
Arrays.stream(sysUser.getUserPermission().split(",")).forEach(info::addStringPermission);
}
return info;
}
}
代码解析:
ShiroRealm 继承了 AuthorizingRealm,必须覆写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法。通过覆写 supports 方法,指定 authenticationToken 必须是我们刚才定义的 ShiroAuthToken 的实例。
doGetAuthenticationInfo 的方法主要是从 authenticationToken 取得 Token,并进行 Token 验证和用户授权。
doGetAuthorizationInfo 的方法主要是实现用户角色、用户权限的配置,对于没有用户角色、权限的系统来说,可以不实现,直接 super。
实现 Token 的拦截器。
ShiroAuthFilter :Shiro 的拦截器,拦截和验证 Token 的有效性
public class ShiroAuthFilter extends BasicHttpAuthenticationFilter { /**
* // 存储Token的H Headers Key
*/
protected static final String AUTHORIZATION_HEADER = "Authorization";
/**
* Token 的开头部分
*/
protected static final String BEARER = "Bearer ";
private String token;
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
// 设置 主题
// 自动调用 ShiroRealm 进行 Token 检查
this.getSubject(request, response).login(new ShiroAuthToken(this.token));
return true;
}
/** 是否允许访问
* @param request Request
* @param response Response
* @param mappedValue mapperValue
* @return true 表示允许放翁
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// Request 中存在 Token
if (this.getAuthzHeader(request) != null) {
try {
executeLogin(request, response);
// 刷新 Token 1, Token 未过期,每次都调用 refreshToken 判断是否需要刷新 Token
TokenUtils tokenUtils = new TokenUtils();
String refreshToken = tokenUtils.refreshToken(this.token);
if (refreshToken != null) {
this.token = refreshToken;
shiroAuthResponse(response, true);
}
return true;
} catch (Exception e) {
// 刷新 Token 2, Token 已经过期,如果过期是在规定时间内则刷新 Token
TokenUtils tokenUtils = new TokenUtils();
String refreshToken = tokenUtils.refreshToken(this.token);
if (refreshToken != null) {
this.token = refreshToken.substring(BEARER.length());
// 重新调用 executeLogin 授权
executeLogin(request, response);
shiroAuthResponse(response, true);
return true;
} else {
// Token 刷新失败没得救或者非法 Token
shiroAuthResponse(response, false);
return false;
}
}
} else {
// Token 不存在,返回未授权信息
shiroAuthResponse(response, false);
return false;
}
}
/** Token 预处理,从 Request 的 Header 取得 Token
* @param request ServletRequest
* @return token or null
*/
@Override
protected String getAuthzHeader(ServletRequest request) {
try {
// header 是否存在 Token
HttpServletRequest httpRequest = WebUtils.toHttp(request);
this.token = httpRequest.getHeader(AUTHORIZATION_HEADER).substring(BEARER.length());
return this.token;
} catch (Exception e) {
return null;
}
}
/** 未授权访问或者 Header 添加 Token
* @param response Response
* @param refresh 是否是刷新 Token
*/
private void shiroAuthResponse(ServletResponse response, boolean refresh) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
if (refresh) {
// 刷新 Token,设置返回的头部
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
httpServletResponse.addHeader(AUTHORIZATION_HEADER, BEARER + this.token);
} else {
// 设置 HTTP 状态码为 401
httpServletResponse.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
// 设置 Json 格式返回
httpServletResponse.setContentType("application/json;charset=UTF-8");
try {
// PrintWriter 输出 Response 返回信息
PrintWriter writer = httpServletResponse.getWriter();
ObjectMapper mapper = new ObjectMapper();
MyResponse myResponse = new MyResponse("error", "非授权访问");
// 将对象输出为 JSON 格式。可以通过重写 MyResponse 的 toString() ,直接通过 myResponse.toString() 即可
writer.write(mapper.writeValueAsString(myResponse));
} catch (IOException e) {
// 打印日志
}
}
}
}
Token 刷新策略:目前小先想到的 Token 刷新策略有以下几种
- 后端提供一个刷新 Token 的接口,前端根据浏览器缓存的 token 过期时间,如 Token 不到 1 天就要过期就访问刷新 Token 的接口。前端实现无,后端实现参考用户登录部分接口和 Token 刷新代码部分
- 后端判断 Token 快要过期了就刷新 Token ,并放入到 Response 的 Header,详情看代码
// 刷新 Token 1
,坏处是每次都要判断是否要刷新 token - 后端判断 Token 在 Token 过期后,如果在指定的时间范围内,则可以刷新 Token,并把新 Token 放入到 Response 的 Header,详情看代码
// 刷新 Token 2
,坏处是要自己手动判断 Token 是否合法 - 其他,还没有想到,欢迎您的留言
回到 TokenUtils 这个类,新增 refreshToken 的方法,特别要注意代码注释中的 // TODO 需要自己用 RSA 算法验证 Token 的合法性
这一部分,如果没有用加密算法验证 Token 是不是自己签发的,伪造的 Token 可以通过方法三骗取合法 Token,感兴趣的读者可以自行试试。
/** Token 刷新 * @param token 就 Token
* @return String 新 Token 或者 null
*/
public String refreshToken(String token) {
try {
// 解密 Token,获取 Claims 主体
Claims claims = Jwts.parserBuilder()
// 设置公钥解密,以为私钥是保密的,因此 Token 只能是自己生成的,如此来验证 Token
.setSigningKey(RsaUtils.getPublicKey())
.build().parseClaimsJws(token).getBody();
// 刷新 Token 1 下面代码是未到期刷新
// 可以更改代码,在验证的 Token 的时候直接判断是否要刷新 Token
assert claims != null;
// Token 过期时间
Date expiration = claims.getExpiration();
// 如果 1 分钟内过期,则刷新 Token
if (!expiration.before(new Date(System.currentTimeMillis() + 60 * 1000))) {
// 不用刷新
return null;
}
SysUser sysUser = new SysUser();
sysUser.setUsername(claims.getAudience());
sysUser.setUserRole(claims.get("role").toString());
sysUser.setUserPermission(claims.get("permission").toString());
// 生成新的 Token
return createToken(sysUser);
} catch (ExpiredJwtException e) {
// 刷新 Token 2 :Token 在解密的时候会自动判断是否过期
// 过期 ExpiredJwtException 可以通过 e.getClaims() 取得 claims
// 实际中千万不要直接这么用
// TODO 需要自己用 RSA 算法验证 Token 的合法性
try {
Claims claims = e.getClaims();
// 如果 claims 不为空表示 Token 正常解析出了主题部分
assert claims != null;
// Token 过期时间
Date expiration = claims.getExpiration();
// 如果过期时间在 10 分钟内,则刷新 Token
if (!expiration.after(new Date(System.currentTimeMillis() - 10 * 60 * 1000))) {
// 超过 10 分钟,没得救了
return null;
} else {
SysUser sysUser = new SysUser();
sysUser.setUsername(claims.getAudience());
sysUser.setUserRole(claims.get("role").toString());
sysUser.setUserPermission(claims.get("permission").toString());
return createToken(sysUser);
}
} catch (Exception e1) {
return null;
}
}
}
ShiroConfig :配置 Shiro 的 Realm 和拦截器、拦截规则,关闭 Session 。对于 Shiro 而言,必须配置名称为 securityManager 和 shiroFilterFactoryBean 的 Bean,Shiro 的启动器 Starter 的应该怎么配置在研究中。不加会报如下错误:
required a bean named "shiroFilterFactoryBean" that could not be found.
ShiroConfig 代码:
@Configurationpublic class ShiroConfig {
/** 使用自定义的 Realm 和关闭 Session 管理器
* @param realm 自定义的 Realm
* @return SecurityManager
*/
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 使用自己的 realm
manager.setRealm(realm);
// 关闭 Session
// shiro.ini 方式参考 http://shiro.apache.org/session-management.html#disabling-subject-state-session-storage
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
/** 添加拦截器和配置拦截规则
* @param securityManager 安全管理器
* @return 拦截器和拦截规则
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = new HashMap<>(2);
// 添加 shiroAuthFilter 的拦截器,不要使用 Spring 来管理 Bean
filters.put("authFilter", new ShiroAuthFilter());
factoryBean.setFilters(filters);
// 一定要用 LinkedHashMap,HashMap 顺序不一定按照 put 的顺序,拦截匹配规则是从上往下的
// 比如 /api/user/login ,已经匹配到了,即使用 anon 的拦截器,就不会再去匹配 /** 了
// anon 支持匿名访问的拦截器
LinkedHashMap<String, String> filterChainDefinitions = new LinkedHashMap<>(4);
// 登录接口和注册放开
filterChainDefinitions.put("/api/user/login", "anon");
filterChainDefinitions.put("/api/user/register", "anon");
// 其他请求通过自定义的 authFilter
filterChainDefinitions.put("/**", "authFilter");
factoryBean.setFilterChainDefinitionMap(filterChainDefinitions);
return factoryBean;
}
}
2.4)登录和注册接口
SysUserService:API 接口服务层
@Servicepublic class SysUserService {
/** Hash 加密的盐 **/
private final String SALT = "#4d1*dlmmddewd@34%";
@Resource private TokenUtils tokenUtils;
@Resource private SysUserMapper sysUserMapper;
/** 用户登录 **/
public MyResponse login(SysUser sysUser) {
// 从 数据库查询用户信息
SysUser user = sysUserMapper.selectByUsername(sysUser.getUsername());
if (user == null || user.getUsername() == null || user.getPassword() == null
|| user.getUserRole() == null || user.getUserPermission() == null) {
return new MyResponse("error", "用户信息不存在");
}
String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
if (!password.equals(user.getPassword())) {
return new MyResponse("error", "密码错误");
}
// 生成 Token
return new MyResponse("SUCCESS",
tokenUtils.createToken(user));
}
/** 用户注册
* @param sysUser 用户注册信息
* @return 用户注册结果
*/
public MyResponse save(SysUser sysUser) throws DataAccessException {
try {
// 密码加密存储
String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
sysUserMapper.insert(sysUser);
} catch (DataAccessException e) {
return new MyResponse("ERROR", "已经存在该用户名或者用户昵称,或者用户权限出错");
}
return new MyResponse("SUCCESS", "用户新增成功");
}
}
这里登录逻辑没有使用 Shiro 的 Realm 来实现,密码存储采用 SHA-512 算法加密用户名存储。对于 Shiro 密码服务功能还在探索中。
SysUserController:API 登录和注册接口
@RestController@RequestMapping("/api/user")
public class SysUserController {
/** 存储Token的H Headers Key **/
protected static final String AUTHORIZATION_HEADER = "Authorization";
@Resource SysUserService sysUserService;
/** 用户登录接口
* @param sysUser 用户登录的用户名和密码
* @return 用户Token和角色
*/
@PostMapping(value = "/login")
public MyResponse login(@RequestBody final SysUser sysUser, ServletResponse response) {
MyResponse myResponse = sysUserService.login(sysUser);
// 如果登录成功
// 将 Token 写入到 Response 的 Header,方便前端刷新 Token 从 Header 取值
if ("SUCCESS".equals(myResponse.getStatus())) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.addHeader(AUTHORIZATION_HEADER, myResponse.getMessage());
}
return myResponse;
}
@PostMapping("/register")
public MyResponse register(@RequestBody SysUser sysUser) {
return sysUserService.save(sysUser);
}
@GetMapping("/hello")
public String hello() {
return "已经登录的用户可见";
}
}
运行项目,访问注册和登录的 API,注册 JSON 参考:
{ "username": "user",
"password": "spring",
"userRole": "USER",
"userPermission":"writer,read"
}
为了方便测试,Token 的有效期设置为 2 分钟,对于过期时间在 1 分钟内或者过期 10 分钟的 Token,访问 /api/user/hello 接口会在 Response 的 Header 中返回新的 Token。
2.5)Shiro 的权限和角色
SysUserController:API 登录和注册接口新加如下的接口:
@RequiresRoles("ADMIN")@PostMapping("/admin")
public String admin() {
return "Admin 的用户角色可以见";
}
@RequiresPermissions("update")
@GetMapping("/permission")
public String permission() {
return "需要 update 的权限才能访问";
}
Shiro 角色和权限的设置在 ShiroRealm 的 doGetAuthorizationInfo 的方法中。
重新运行项目,分别用不同的角色和权限的用户访问 admin 和 permission 接口。
小结
这一章中,主要实现了 Spring Boot 整合 Shiro 实现 Token 的登录和验证,以及角色和权限的访问控制。下面的文章安排如下:
- 微信扫码登录
- Spring Boot 的异常拦截:统一拦截、封装异常信息返回给前端。
附录:Maven 项目配置
1<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->2<!-- 添加如下依赖 -->
3<dependency>
4 <groupId>org.apache.shiro</groupId>
5 <artifactId>shiro-spring-boot-web-starter</artifactId>
6 <version>1.5.2</version>
7</dependency>
以上是 SpringBoot2.X实战Shiro(Token)登录和注册 的全部内容, 来源链接: utcz.com/z/519035.html