点击上方“编程技术进阶”,选择加"星标"或“置顶”
重磅干货,第一时间送达
博客主页:https://me.csdn.net/u010974701
源代码仓库:https://github.com/zhshuixian/learn-spring-boot-2
在上一节“Spring Security (Token)登录和注册”中,主要介绍了 Spring Boot 整合 Spring Security 实现 Token 的登录和认证,这一小节中,我们将实现 Spring Boot 整合 Shiro 实现 Token 的登录和认证。
目录结构
1)Apache Shiro 简介2)Shiro 项目配置2.1)项目配置3)开始使用 Shiro2.1)实体类 Entity 和 Mapper2.2)Token 配置2.3)Shiro 配置2.4)登录和注册接口2.5)Shiro 的权限和角色
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 项目配置
1 implementation 'org.springframework.boot:spring-boot-starter-web'
2 implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.2'
3 runtimeOnly 'mysql:mysql-connector-java'
4 testImplementation 'org.springframework.boot:spring-boot-starter-test'
5 // https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter
6 compile group: 'org.apache.shiro', name: 'shiro-spring-boot-web-starter', version: '1.5.2'
7 // https://github.com/jwtk/jjwt
8 compile 'io.jsonwebtoken:jjwt-api:0.11.1'
9 runtime 'io.jsonwebtoken:jjwt-impl:0.11.1',
10 // Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
11 //'org.bouncycastle:bcprov-jdk15on:1.60',
12 // or 'io.jsonwebtoken:jjwt-gson:0.11.1' for gson
13 'io.jsonwebtoken:jjwt-jackson:0.11.1'
2.1)项目配置
配置 MySQL 数据库和 MyBatis 驼峰命名转换,application.properties
1# 数据库 URL、用户名、密码、JDBC Driver更换数据库只需更改这些信息即可
2# MySQL 8 需要指定 serverTimezone 才能连接成功
3spring.datasource.url=jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
4spring.datasource.password=xiaoxian
5spring.datasource.username=root
6spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
7# MyBatis 驼峰命名转换
8mybatis.configuration.map-underscore-to-camel-case=true
添加 @MapperScan
1@MapperScan("org.xian.security.mapper")
2public class SecurityApplication {}
3)开始使用 Shiro
项目的主要结构:
controller 包:API 接口
service 包:为 API 提供接口服务
mapper 包:MyBatis Mapper 类
entity 包:实体类
Shiro 包:Token 拦截验证、Token 生成、Shiro 的 Realm 等配置
MyResponse :公共 Response 返回消息类:
1public class MyResponse implements Serializable {
2 private static final long serialVersionUID = -2L;
3 private String status;
4 private String message;
5}
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
1create table sys_user
2(
3 user_id bigint auto_increment,
4 username varchar(18) not null unique,
5 password varchar(128) not null,
6 user_role varchar(8) not null,
7 user_permission varchar(36) not null,
8 constraint sys_user_pk
9 primary key (user_id)
10);
Entity 实体类:新建 package,名称为 entity 。在 entity下新建一个 SysUser 类:
1public class SysUser implements Serializable {
2 private static final long serialVersionUID = 4522943071576672084L;
3 private Long userId;
4 private String username;
5 private String password;
6 private String userRole;
7 private String userPermission;
8 // 省略 getter setter constructor
9}
Mapper 接口类:新建包 mapper,新建 SysUserMapper 类:
1public interface SysUserMapper {
2 /** 往 sys_user 插入一条记录
3 * @param sysUser 用户信息
4 */
5 @Insert("Insert Into sys_user(username, password,user_role,user_permission) Values(#{username}, #{password},#{userRole},#{userPermission})")
6 @Options(useGeneratedKeys = true, keyProperty = "userId")
7 void insert(SysUser sysUser);
8 /** 根据用户 Username 查询用户信息
9 * @param username 用户名
10 * @return 用户信息
11 */
12 @Select("Select user_id,username, password,user_role,user_permission From sys_user Where username=#{username}")
13 SysUser selectByUsername(String username);
14}
2.2)Token 配置
首先实现 Token 生成和验证的功能:
RSA 密钥公钥工具类
Token 生成、验证工具类
在 shiro 包下新建 RsaUtils 类,RSA 的公钥和密钥的工具类。注意在 JDK 8 中,2048 位的密钥不受支持。代码参考Spring Security (Token)登录和注册 的 2.2)Token 配置 的小节:
TokenUtils : 生成和验证 Token 的工具类。可选的 Token 主体部分是指在验证和授权的时候用不上这些信息,主要的代码和上一节差不多,主要是增加一个 Refresh 刷新 Token 的功能,Token 刷新部分在后面单独来讲。
1@Component
2public class TokenUtils implements Serializable {
3 private static final long serialVersionUID = -3L;
4 /** Token 有效时长 多少秒 **/
5 private static final Long EXPIRATION = 2 * 60L;
6
7 /** 生成 Token 字符串 setAudience 接收者 setExpiration 过期时间 role 用户角色
8 * @param sysUser 用户信息
9 * @return 生成的Token字符串 or null
10 */
11 public String createToken(SysUser sysUser) {
12 try {
13 // Token 的过期时间
14 Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION * 1000);
15 // 生成 Token
16 String token = Jwts.builder()
17 // 设置 Token 签发者 可选
18 .setIssuer("SpringBoot")
19 // 根据用户名设置 Token 的接受者
20 .setAudience(sysUser.getUsername())
21 // 设置过期时间
22 .setExpiration(expirationDate)
23 // 设置 Token 生成时间 可选
24 .setIssuedAt(new Date())
25 // 通过 claim 方法设置一个 key = role,value = userRole 的值
26 .claim("role", sysUser.getUserRole())
27 // 用户角色
28 // 通过 claim 方法设置一个 key = permission,value = Permission 的值
29 .claim("permission", sysUser.getUserPermission())
30 // 设置加密密钥和加密算法,注意要用私钥加密且保证私钥不泄露
31 .signWith(RsaUtils.getPrivateKey(), SignatureAlgorithm.RS256)
32 .compact();
33 return String.format("Bearer %s", token);
34 } catch (Exception e) {
35 return null;
36 }
37 }
38
39 /** 验证 Token ,并获取到用户名和用户权限信息
40 * @param token Token 字符串
41 * @return sysUser 用户信息
42 */
43 public SysUser validationToken(String token) {
44 try {
45 // 解密 Token,获取 Claims 主体
46 Claims claims = Jwts.parserBuilder()
47 // 设置公钥解密,以为私钥是保密的,因此 Token 只能是自己生成的,如此来验证 Token
48 .setSigningKey(RsaUtils.getPublicKey())
49 .build().parseClaimsJws(token).getBody();
50 assert claims != null;
51 SysUser sysUser = new SysUser();
52 // 获得用户信息
53 sysUser.setUsername(claims.getAudience());
54 sysUser.setUserRole(claims.get("role").toString());
55 sysUser.setUserPermission(claims.get("permission").toString());
56 return sysUser;
57 } catch (Exception e) {
58 return null;
59 }
60 }
61}
2.3)Shiro 配置
实现 Shiro 的 Realm,拦截器等
ShiroAuthToken :实现 AuthenticationToken 接口
ShiroRealm :自定义 Realm,验证 Token 和从 Token 中取得用户角色和权限
ShiroAuthFilter :拦截器,拦截所有请求,并验证 Token
ShiroConfig :Shiro 配置,将 Realm、拦截器等配置到 SecurityManager 中
ShiroAuthToken :实现 AuthenticationToken 接口,作为 Token 传入到 Realm 的载体:
1public class ShiroAuthToken implements AuthenticationToken {
2 private String token;
3 public ShiroAuthToken(String token) { this.token = token; }
4
5 @Override
6 public Object getPrincipal() { return token; }
7
8 @Override
9 public Object getCredentials() { return token; }
10}
ShiroRealm :从 ShiroAuthToken 取得 Token 并进行身份验证和角色权限配置。
1@Service
2public class ShiroRealm extends AuthorizingRealm {
3 @Resource
4 TokenUtils tokenUtils;
5
6 @Override
7 public boolean supports(AuthenticationToken authenticationToken) {
8 // 指定当前 authenticationToken 需要为 ShiroAuthToken 的实例
9 return authenticationToken instanceof ShiroAuthToken;
10 }
11
12 @Override
13 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
14 ShiroAuthToken shiroAuthToken = (ShiroAuthToken) authenticationToken;
15 String token = (String) shiroAuthToken.getCredentials();
16 // 验证 Token
17 SysUser sysUser = tokenUtils.validationToken(token);
18 if (sysUser == null || sysUser.getUsername() == null || sysUser.getUserRole() == null) {
19 throw new AuthenticationException("Token 无效");
20 }
21 return new SimpleAuthenticationInfo(token,
22 token, "ShiroRealm");
23 }
24
25 @Override
26 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
27 // 获取用户信息
28 SysUser sysUser = tokenUtils.validationToken(principals.toString());
29 // 创建一个授权对象
30 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
31 // 判断用户角色是否存在
32 if (!sysUser.getUserRole().isEmpty()) {
33 // 角色设置
34 info.addRole(sysUser.getUserRole());
35 }
36 if (!sysUser.getUserPermission().isEmpty()) {
37 // 进行权限设置,根据 , 分割
38 Arrays.stream(sysUser.getUserPermission().split(",")).forEach(info::addStringPermission);
39 }
40 return info;
41 }
42}
代码解析:
ShiroRealm 继承了 AuthorizingRealm,必须覆写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 两个方法。通过覆写 supports 方法,指定 authenticationToken 必须是我们刚才定义的 ShiroAuthToken 的实例。
doGetAuthenticationInfo 的方法主要是从 authenticationToken 取得 Token,并进行 Token 验证和用户授权。
doGetAuthorizationInfo 的方法主要是实现用户角色、用户权限的配置,对于没有用户角色、权限的系统来说,可以不实现,直接 super。
实现 Token 的拦截器。
ShiroAuthFilter :Shiro 的拦截器,拦截和验证 Token 的有效性
1public class ShiroAuthFilter extends BasicHttpAuthenticationFilter {
2
3 /**
4 * // 存储Token的H Headers Key
5 */
6 protected static final String AUTHORIZATION_HEADER = "Authorization";
7
8 /**
9 * Token 的开头部分
10 */
11 protected static final String BEARER = "Bearer ";
12
13 private String token;
14
15
16 @Override
17 protected boolean executeLogin(ServletRequest request, ServletResponse response) {
18 // 设置 主题
19 // 自动调用 ShiroRealm 进行 Token 检查
20 this.getSubject(request, response).login(new ShiroAuthToken(this.token));
21 return true;
22 }
23
24 /** 是否允许访问
25 * @param request Request
26 * @param response Response
27 * @param mappedValue mapperValue
28 * @return true 表示允许放翁
29 */
30 @Override
31 protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
32 // Request 中存在 Token
33 if (this.getAuthzHeader(request) != null) {
34 try {
35 executeLogin(request, response);
36 // 刷新 Token 1, Token 未过期,每次都调用 refreshToken 判断是否需要刷新 Token
37 TokenUtils tokenUtils = new TokenUtils();
38 String refreshToken = tokenUtils.refreshToken(this.token);
39 if (refreshToken != null) {
40 this.token = refreshToken;
41 shiroAuthResponse(response, true);
42 }
43 return true;
44 } catch (Exception e) {
45 // 刷新 Token 2, Token 已经过期,如果过期是在规定时间内则刷新 Token
46 TokenUtils tokenUtils = new TokenUtils();
47 String refreshToken = tokenUtils.refreshToken(this.token);
48 if (refreshToken != null) {
49 this.token = refreshToken.substring(BEARER.length());
50 // 重新调用 executeLogin 授权
51 executeLogin(request, response);
52 shiroAuthResponse(response, true);
53 return true;
54 } else {
55 // Token 刷新失败没得救或者非法 Token
56 shiroAuthResponse(response, false);
57 return false;
58 }
59 }
60 } else {
61 // Token 不存在,返回未授权信息
62 shiroAuthResponse(response, false);
63 return false;
64 }
65 }
66
67 /** Token 预处理,从 Request 的 Header 取得 Token
68 * @param request ServletRequest
69 * @return token or null
70 */
71 @Override
72 protected String getAuthzHeader(ServletRequest request) {
73 try {
74 // header 是否存在 Token
75 HttpServletRequest httpRequest = WebUtils.toHttp(request);
76 this.token = httpRequest.getHeader(AUTHORIZATION_HEADER).substring(BEARER.length());
77 return this.token;
78 } catch (Exception e) {
79 return null;
80 }
81 }
82
83 /** 未授权访问或者 Header 添加 Token
84 * @param response Response
85 * @param refresh 是否是刷新 Token
86 */
87 private void shiroAuthResponse(ServletResponse response, boolean refresh) {
88 HttpServletResponse httpServletResponse = (HttpServletResponse) response;
89 if (refresh) {
90 // 刷新 Token,设置返回的头部
91 httpServletResponse.setStatus(HttpServletResponse.SC_OK);
92 httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
93 httpServletResponse.addHeader(AUTHORIZATION_HEADER, BEARER + this.token);
94 } else {
95 // 设置 HTTP 状态码为 401
96 httpServletResponse.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
97 // 设置 Json 格式返回
98 httpServletResponse.setContentType("application/json;charset=UTF-8");
99 try {
100 // PrintWriter 输出 Response 返回信息
101 PrintWriter writer = httpServletResponse.getWriter();
102 ObjectMapper mapper = new ObjectMapper();
103 MyResponse myResponse = new MyResponse("error", "非授权访问");
104 // 将对象输出为 JSON 格式。可以通过重写 MyResponse 的 toString() ,直接通过 myResponse.toString() 即可
105 writer.write(mapper.writeValueAsString(myResponse));
106 } catch (IOException e) {
107 // 打印日志
108 }
109 }
110 }
111}
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,感兴趣的读者可以自行试试。
1/** Token 刷新
2 * @param token 就 Token
3 * @return String 新 Token 或者 null
4 */
5public String refreshToken(String token) {
6 try {
7 // 解密 Token,获取 Claims 主体
8 Claims claims = Jwts.parserBuilder()
9 // 设置公钥解密,以为私钥是保密的,因此 Token 只能是自己生成的,如此来验证 Token
10 .setSigningKey(RsaUtils.getPublicKey())
11 .build().parseClaimsJws(token).getBody();
12 // 刷新 Token 1 下面代码是未到期刷新
13 // 可以更改代码,在验证的 Token 的时候直接判断是否要刷新 Token
14 assert claims != null;
15 // Token 过期时间
16 Date expiration = claims.getExpiration();
17 // 如果 1 分钟内过期,则刷新 Token
18 if (!expiration.before(new Date(System.currentTimeMillis() + 60 * 1000))) {
19 // 不用刷新
20 return null;
21 }
22 SysUser sysUser = new SysUser();
23 sysUser.setUsername(claims.getAudience());
24 sysUser.setUserRole(claims.get("role").toString());
25 sysUser.setUserPermission(claims.get("permission").toString());
26 // 生成新的 Token
27 return createToken(sysUser);
28 } catch (ExpiredJwtException e) {
29 // 刷新 Token 2 :Token 在解密的时候会自动判断是否过期
30 // 过期 ExpiredJwtException 可以通过 e.getClaims() 取得 claims
31 // 实际中千万不要直接这么用
32 // TODO 需要自己用 RSA 算法验证 Token 的合法性
33 try {
34 Claims claims = e.getClaims();
35 // 如果 claims 不为空表示 Token 正常解析出了主题部分
36 assert claims != null;
37 // Token 过期时间
38 Date expiration = claims.getExpiration();
39 // 如果过期时间在 10 分钟内,则刷新 Token
40 if (!expiration.after(new Date(System.currentTimeMillis() - 10 * 60 * 1000))) {
41 // 超过 10 分钟,没得救了
42 return null;
43 } else {
44 SysUser sysUser = new SysUser();
45 sysUser.setUsername(claims.getAudience());
46 sysUser.setUserRole(claims.get("role").toString());
47 sysUser.setUserPermission(claims.get("permission").toString());
48 return createToken(sysUser);
49 }
50 } catch (Exception e1) {
51 return null;
52 }
53 }
54}
ShiroConfig :配置 Shiro 的 Realm 和拦截器、拦截规则,关闭 Session 。对于 Shiro 而言,必须配置名称为 securityManager 和 shiroFilterFactoryBean 的 Bean,Shiro 的启动器 Starter 的应该怎么配置在研究中。不加会报如下错误:
1required a bean named 'shiroFilterFactoryBean' that could not be found.
ShiroConfig 代码:
1@Configuration
2public class ShiroConfig {
3 /** 使用自定义的 Realm 和关闭 Session 管理器
4 * @param realm 自定义的 Realm
5 * @return SecurityManager
6 */
7 @Bean
8 public DefaultWebSecurityManager securityManager(ShiroRealm realm) {
9 DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
10 // 使用自己的 realm
11 manager.setRealm(realm);
12 // 关闭 Session
13 // shiro.ini 方式参考 http://shiro.apache.org/session-management.html#disabling-subject-state-session-storage
14 DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
15 defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
16 DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
17 subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
18 manager.setSubjectDAO(subjectDAO);
19 return manager;
20 }
21
22 /** 添加拦截器和配置拦截规则
23 * @param securityManager 安全管理器
24 * @return 拦截器和拦截规则
25 */
26 @Bean
27 public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
28 ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
29 factoryBean.setSecurityManager(securityManager);
30 Map<String, Filter> filters = new HashMap<>(2);
31 // 添加 shiroAuthFilter 的拦截器,不要使用 Spring 来管理 Bean
32 filters.put("authFilter", new ShiroAuthFilter());
33 factoryBean.setFilters(filters);
34 // 一定要用 LinkedHashMap,HashMap 顺序不一定按照 put 的顺序,拦截匹配规则是从上往下的
35 // 比如 /api/user/login ,已经匹配到了,即使用 anon 的拦截器,就不会再去匹配 /** 了
36 // anon 支持匿名访问的拦截器
37 LinkedHashMap<String, String> filterChainDefinitions = new LinkedHashMap<>(4);
38 // 登录接口和注册放开
39 filterChainDefinitions.put("/api/user/login", "anon");
40 filterChainDefinitions.put("/api/user/register", "anon");
41 // 其他请求通过自定义的 authFilter
42 filterChainDefinitions.put("/**", "authFilter");
43 factoryBean.setFilterChainDefinitionMap(filterChainDefinitions);
44 return factoryBean;
45 }
46}
2.4)登录和注册接口
SysUserService:API 接口服务层
1@Service
2public class SysUserService {
3 /** Hash 加密的盐 **/
4 private final String SALT = "#4d1*dlmmddewd@34%";
5 @Resource private TokenUtils tokenUtils;
6 @Resource private SysUserMapper sysUserMapper;
7
8 /** 用户登录 **/
9 public MyResponse login(SysUser sysUser) {
10 // 从 数据库查询用户信息
11 SysUser user = sysUserMapper.selectByUsername(sysUser.getUsername());
12 if (user == null || user.getUsername() == null || user.getPassword() == null
13 || user.getUserRole() == null || user.getUserPermission() == null) {
14 return new MyResponse("error", "用户信息不存在");
15 }
16 String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
17 if (!password.equals(user.getPassword())) {
18 return new MyResponse("error", "密码错误");
19 }
20 // 生成 Token
21 return new MyResponse("SUCCESS",
22 tokenUtils.createToken(user));
23 }
24
25 /** 用户注册
26 * @param sysUser 用户注册信息
27 * @return 用户注册结果
28 */
29 public MyResponse save(SysUser sysUser) throws DataAccessException {
30 try {
31 // 密码加密存储
32 String password = new SimpleHash("SHA-512", sysUser.getPassword(), this.SALT).toString();
33 sysUserMapper.insert(sysUser);
34 } catch (DataAccessException e) {
35 return new MyResponse("ERROR", "已经存在该用户名或者用户昵称,或者用户权限出错");
36 }
37 return new MyResponse("SUCCESS", "用户新增成功");
38 }
39}
这里登录逻辑没有使用 Shiro 的 Realm 来实现,密码存储采用 SHA-512 算法加密用户名存储。对于 Shiro 密码服务功能还在探索中。
SysUserController:API 登录和注册接口
1@RestController
2@RequestMapping("/api/user")
3public class SysUserController {
4 /** 存储Token的H Headers Key **/
5 protected static final String AUTHORIZATION_HEADER = "Authorization";
6 @Resource SysUserService sysUserService;
7
8 /** 用户登录接口
9 * @param sysUser 用户登录的用户名和密码
10 * @return 用户Token和角色
11 */
12 @PostMapping(value = "/login")
13 public MyResponse login(@RequestBody final SysUser sysUser, ServletResponse response) {
14 MyResponse myResponse = sysUserService.login(sysUser);
15 // 如果登录成功
16 // 将 Token 写入到 Response 的 Header,方便前端刷新 Token 从 Header 取值
17 if ("SUCCESS".equals(myResponse.getStatus())) {
18 HttpServletResponse httpServletResponse = (HttpServletResponse) response;
19 httpServletResponse.setStatus(HttpServletResponse.SC_OK);
20 httpServletResponse.addHeader(AUTHORIZATION_HEADER, myResponse.getMessage());
21 }
22 return myResponse;
23 }
24
25 @PostMapping("/register")
26 public MyResponse register(@RequestBody SysUser sysUser) {
27 return sysUserService.save(sysUser);
28 }
29
30 @GetMapping("/hello")
31 public String hello() {
32 return "已经登录的用户可见";
33 }
34}
运行项目,访问注册和登录的 API,注册 JSON 参考:
1{
2 "username": "user",
3 "password": "spring",
4 "userRole": "USER",
5 "userPermission":"writer,read"
6}
为了方便测试,Token 的有效期设置为 2 分钟,对于过期时间在 1 分钟内或者过期 10 分钟的 Token,访问 api/user/hello 接口会在 Response 的 Header 中返回新的 Token。

2.5)Shiro 的权限和角色
SysUserController:API 登录和注册接口新加如下的接口:
1@RequiresRoles("ADMIN")
2@PostMapping("/admin")
3public String admin() {
4 return "Admin 的用户角色可以见";
5}
6
7@RequiresPermissions("update")
8@GetMapping("/permission")
9public String permission() {
10 return "需要 update 的权限才能访问";
11}
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>
- MORE | 更多精彩文章 -
求求你!不要在网上乱拷贝代码了!一段网上找的代码突然炸了,项目出现大BUG
如果你喜欢本文,
请长按二维码,关注 编程技术进阶

转发至朋友圈,是对我最大的支持。
好文章,我在看❤️




