暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Spring Boot 2.X 实战--Shiro (Token)登录和验证

编程技术进阶 2020-04-14
1228

点击上方“编程技术进阶”,选择加"星标"或“置顶”

重磅干货,第一时间送达

博客主页: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_idbigint自增主键
usernamevarchar(18)用户名,非空唯一
passwordvarchar(128)密码,非空
user_rolevarchar(8)用户角色(USER ADMIN)
user_permissionvarchar(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(128not 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 | 更多精彩文章 -

IDEA 2020.1版发布

求求你!不要在网上乱拷贝代码了!一段网上找的代码突然炸了,项目出现大BUG

如果你喜欢本文,

请长按二维码,关注 编程技术进阶

转发至朋友圈,是对我最大的支持。

好文章,我在看❤️




文章转载自编程技术进阶,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论