简介

说明


  1. 本文实操Shiro的使用。尽量使用原生的shiro配置,尽量少自定义配置。
  2. 使用jwt替代默认的authc作为认证方式,其他不变。
  3. 我自测通过,代码可用。

shiro用法我写了一个系列,

使用形式


  1. 使用jwt替代默认的session来管理权限。
  1. 自定义jwt过滤器,并将其以“authc”为名字注册到Spring容器
  1. 使用角色和资源权限两种方式。(注解的方式)
  2. 使用Knife4j测试(Swagger的升级版)。
  3. 使用shiro-spring-boot-web-starter:1.7.0

技术栈


  1. shiro-spring-boot-web-starter:1.7.0
  2. spring-boot-starter-parent:2.3.8.RELEASE
  3. mysql:8.0
  4. mybatis-plus-boot-starter:3.4.3.1(持久层框架)
  5. lombok(简化代码)
  6. knife4j-spring-boot-starter:3.0.3(接口文档(swagger的升级版))

业务场景

本文假设做一个商城系统。

角色如下


  • admin:有所有权限
  • 使用shiro通配符:*:*
  • productManager:有product的所有权限

  • product:add,product:delete,product:edit,product:view
  • 本处把这四个权限单独赋予productManager这个角色,实际项目里可以将product:*这个权限赋予productManager这个角色。

  • orderManager:有order的所有权限

  • order:add,product:delete,product:edit,product:view
  • 本处把这四个权限单独赋予productManager这个角色,实际项目里可以将product:*这个权限赋予productManager这个角色。


用户密码及权限如下


用户



密码



角色



拥有的权限



zhang3



12345



admin



所有权限。



li4



abcde



productManager



产品的所有权限。


本处为了测试功能,赋予以下特殊权限:


  • 验证shiro可单独通过角色来控制权限

    • 给编辑订单接口设置角色权限:

      • @RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)


  • 验证shiro无法在一个接口上同时加@RequiresRoles和@RequiresPermissions

    • 给查看订单的接口设置角色权限与资源权限:

      • @RequiresPermissions("order:view")
        @RequiresRoles(value = {"admin,productManager"}, logical = Logical.OR)



代码

项目结构

Shiro--整合jwt--使用/教程/实例_spring

表结构及数据

        为了简化代码,用户、角色、权限直接用SQL生成到数据库,实际开发中是要通过Controller去增删改的。

Shiro--整合jwt--使用/教程/实例_apache_02

business.sql

DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;

DROP TABLE IF EXISTS t_user;
DROP TABLE IF EXISTS t_role;
DROP TABLE IF EXISTS t_permission;
DROP TABLE IF EXISTS t_user_role_mid;
DROP TABLE IF EXISTS t_role_permission_mid;

create table t_user (
id bigint AUTO_INCREMENT,
user_name VARCHAR(100),
password VARCHAR(100),
salt VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_role (
id bigint AUTO_INCREMENT,
name VARCHAR(100),
description VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_permission (
id bigint AUTO_INCREMENT,
name VARCHAR(100),
description VARCHAR(100),
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_user_role_mid (
id bigint AUTO_INCREMENT,
user_id bigint,
role_id bigint,
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

create table t_role_permission_mid (
id bigint AUTO_INCREMENT,
role_id bigint,
permission_id bigint,
PRIMARY KEY(id)
) charset=utf8 ENGINE=InnoDB;

-- 密码:12345
INSERT INTO `t_user` VALUES (1,''zhang3'',''a7d59dfc5332749cb801f86a24f5f590'',''e5ykFiNwShfCXvBRPr3wXg=='');
-- 密码:abcde
INSERT INTO `t_user` VALUES (2,''li4'',''43e28304197b9216e45ab1ce8dac831b'',''jPz19y7arvYIGhuUjsb6sQ=='');
INSERT INTO `t_role` VALUES (1,''admin'',''超级管理员'');
INSERT INTO `t_role` VALUES (2,''productManager'',''产品管理员'');
INSERT INTO `t_role` VALUES (3,''orderManager'',''订单管理员'');
INSERT INTO `t_permission` VALUES (1,''*:*'',''所有权限'');
INSERT INTO `t_permission` VALUES (2,''product:add'',''增加产品'');
INSERT INTO `t_permission` VALUES (3,''product:delete'',''删除产品'');
INSERT INTO `t_permission` VALUES (4,''product:edit'',''编辑产品'');
INSERT INTO `t_permission` VALUES (5,''product:view'',''查看产品'');
INSERT INTO `t_permission` VALUES (6,''order:add'',''增加订单'');
INSERT INTO `t_permission` VALUES (7,''order:delete'',''删除订单'');
INSERT INTO `t_permission` VALUES (8,''order:edit'',''编辑订单'');
INSERT INTO `t_permission` VALUES (9,''order:view'',''查看订单'');
INSERT INTO `t_user_role_mid` VALUES (1,2,2);
INSERT INTO `t_user_role_mid` VALUES (2,1,1);
INSERT INTO `t_role_permission_mid` VALUES (1,1,1);
INSERT INTO `t_role_permission_mid` VALUES (2,2,2);
INSERT INTO `t_role_permission_mid` VALUES (3,2,3);
INSERT INTO `t_role_permission_mid` VALUES (4,2,4);
INSERT INTO `t_role_permission_mid` VALUES (5,2,5);
INSERT INTO `t_role_permission_mid` VALUES (6,3,6);
INSERT INTO `t_role_permission_mid` VALUES (7,3,7);
INSERT INTO `t_role_permission_mid` VALUES (8,3,8);
INSERT INTO `t_role_permission_mid` VALUES (9,3,9);

配置文件及依赖

application.yml

spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/shiro?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 222333

# mybatis-plus打印SQL日志
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

shiro:
enabled: true # 开启 shiro,默认为 true
web:
enabled: true # 开启 shiro Web,默认为 true
# loginUrl: /login # 登录地址,默认为"login.jsp"
# successUrl: /index # 登录成功后跳转的地址,默认为"/"
# unauthorizedUrl: /unauthorized # 未获授权默认跳转地址
# sessionManager:
# sessionIdCookieEnabled: true # 是否允许通过 Cookie 实现会话跟踪,默认为 true。
# sessionIdUrlRewritingEnabled: false # 是否将JSESSIONID放到url中,默认为 true。
# annotations:
# enabled: true # 开启shiro的注解。比如:@RequiresRole。
# 开启shiro注解一共有三种方式。
# 1.引入spring aop依赖:org.springframework.boot:spring-boot-starter-aop
# 2.在application.yml里边设置:shiro.annotations.enabled=true。
# 对应:ShiroAnnotationProcessorAutoConfiguration.class
# 3.提供一个AuthorizationAttributeSourceAdvisor这个bean
# 强烈建议使用第1种,因为shiro自带的aop与spring aop只能用一个,如果用了shiro的aop,那么很多
# spring的注解就失效了。比如:@Async,@Cacheable

# 自定义的配置
custom:
jwt:
secret: 7h4alejfloriaj5&asf!a4m # 密钥。随便写
expire: 1800 # token有效时间,30分钟。单位:秒

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>shiro_3_jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>shiro_3_jwt</name>
<description>shiro_3_jwt</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring-boot-web-starter -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>

配置(config包)

代码执行流程(login)

JwtFilter#onAccessDenied  //正常返回true  

   PathMatchingFilter     //去anon过滤器查找,在里边,则放行  

     自己的login接口

代码执行流程(需权限的接口)

JwtFilter#onAccessDenied  //执行executeLogin(servletRequest, servletResponse);  

   JwtFilter#createToken  

     AccountRealm#doGetAuthenticationInfo  

       AccountRealm#doGetAuthorizationInfo  

         自己的接口

shiro总配置

package com.example.demo.config.shiro;

import com.example.demo.common.constant.WhiteList;
import com.example.demo.config.shiro.filter.JwtFilter;
import com.example.demo.config.shiro.realm.AccountRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

chainDefinition.addPathDefinition("/login", "anon");

WhiteList.ALL.forEach(str -> {
chainDefinition.addPathDefinition(str, "anon");
});

// all other paths require a logged in user
chainDefinition.addPathDefinition("/**", "jwt");
return chainDefinition;
}

/**
* 设置过滤器,将自定义的Filter加入。
*/
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
Map<String, Filter> filterMap = factoryBean.getFilters();
filterMap.put("jwt", new JwtFilter());
factoryBean.setFilters(filterMap);
factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap());

return factoryBean;
}

// 这样是不行的,会导致标记了anon的路径也会走到JwtFilter。
// 也就是说:不能将自定义的filter注册成bean。
// @Bean("authc")
// public AuthenticatingFilter authenticatingFilter() {
// return new JwtFilter();
// }

@Bean
public DefaultWebSecurityManager securityManager() {
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
// 关闭shiro自带的session。这样不能通过session登录shiro,后面将采用jwt凭证登录。
// 见:http://shiro.apache.org/session-management.html#SessionManagement-DisablingSubjectStateSessionStorage
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);

DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(getDatabaseRealm());
securityManager.setSubjectDAO(subjectDAO);

return securityManager;
}

@Bean
public AccountRealm getDatabaseRealm() {
return new AccountRealm();
}

/**
* setUsePrefix(true)用于解决一个奇怪的bug。如下:
* 在引入spring aop的情况下,在@Controller注解的类的方法中加入@RequiresRole等
* shiro注解,会导致该方法无法映射请求,导致返回404。加入这项配置能解决这个bug。
*/
@Bean
public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}

/**
* 凭证匹配器
* - 如果密码校验交给Shiro的SimpleAuthenticationInfo进行处理,则需要提供本类,并修改下doGetAuthenticationInfo
* - 本处我登录接口的密码校验是自己写的,所以不需要提供本类
* @return
*/
/* @Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);

return hashedCredentialsMatcher;
}*/

/**
* 开启shiro 注解。比如:@RequiresRole
* 本处不用此方法开启注解,使用引入spring aop依赖的方式。原因见:application.yml里的注释
*/
/*@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor =
new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}*/

/**
* 此种配置方法在本项目中跑不通。
*/
/* @Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 认证失败要跳转的地址。
// shiroFilterFactoryBean.setLoginUrl("/login");
// // 登录成功后要跳转的链接
// shiroFilterFactoryBean.setSuccessUrl("/index");
// // 未授权界面;
// shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");

WhiteList.ALL.forEach(str -> {
filterChainDefinitionMap.put(str, "anon");
});

// filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/**", "jwtAuthc");

Map<String, Filter> customisedFilters = new LinkedHashMap<>();
// 不能用注入来设置过滤器。若用注入,则本过滤器优先级会最高(/**优先级最高,导致前边所有请求都无效)。
// springboot会扫描所有实现了javax.servlet.Filter接口的类,无需加@Component也会扫描到。
customisedFilters.put("jwtAuthc", new JwtFilter());

shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
shiroFilterFactoryBean.setFilters(customisedFilters);

return shiroFilterFactoryBean;
}*/
}

自定义Realm

package com.example.demo.config.shiro.realm;

import com.example.demo.rbac.permission.service.PermissionService;
import com.example.demo.rbac.role.service.RoleService;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.service.UserService;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import com.example.demo.common.entity.AccountProfile;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;

import java.util.Set;

public class AccountRealm extends AuthorizingRealm {
@Lazy
@Autowired
private UserService userService;

@Lazy
@Autowired
private RoleService roleService;

@Lazy
@Autowired
private PermissionService permissionService;

//使realm支持jwt的认证方案
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}

// 登录认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
JwtToken jwtToken = (JwtToken) token;

String userId = JwtUtil.getUserIdByToken((String) jwtToken.getPrincipal());
if (userId == null) {
throw new UnknownAccountException("token为空,请重新登录");
}
// 获取数据库中的密码
User user = userService.getById(userId);
if (user == null) {
throw new UnknownAccountException("token为空,请重新登录");
}

AccountProfile accountProfile = new AccountProfile();
accountProfile.setId(userId);
accountProfile.setUserName(user.getUserName());

String salt = user.getSalt();

// 认证信息里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名:accountRealm
// 盐:通过ShiroConfig里配置的 HashedCredentialsMatcher 进行自动校验。
// 但本处我是直接在登录接口中校验的密码,所以本处盐不提供也可以
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
accountProfile, jwtToken.getCredentials(), ByteSource.Util.bytes(salt), getName());
return authenticationInfo;
}

// 权限验证
// 只有用到org.apache.shiro.web.filter.authz包里默认的过滤器才会走到这里。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 能进入到这里,表示账号已经通过认证了
AccountProfile profile = (AccountProfile) principalCollection.getPrimaryPrincipal();

// 通过service获取角色和权限
Set<String> permissions = permissionService
.getPermissionsByUserId(Long.parseLong(profile.getId()));
Set<String> roles = roleService.getRolesByUserId(profile.getId());

// 授权对象
SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
// 把通过service获取到的角色和权限放进去
s.setStringPermissions(permissions);
s.setRoles(roles);
return s;
}
}

自定义jwt过滤器

package com.example.demo.config.shiro.filter;

import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.util.auth.JwtUtil;
import com.example.demo.config.shiro.entity.JwtToken;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class JwtFilter extends AuthenticatingFilter {
/**
* 所有请求都会到这里来(无论是不是anon)。
* 返回true:表示允许向下走。后边会走PathMatchingFilter,看路径是否对应anon等
* 返回false:表示不允许向下走。
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest,
ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);
// 自定义Header也可以,但浏览器不会存自定义的Header,需要前端自己去存
// String token = request.getHeader("Authentication");

if (!StringUtils.hasText(token)) {
return true;
} else {
boolean verified = JwtUtil.verifyToken(token);
if (!verified) {
return true;
}
}

// 此登录并非调用login接口,而是shiro层面的登录。
// 里边会调用下边的createToken方法
return executeLogin(servletRequest, servletResponse);
}

/**
* 这里的token会传给AuthorizingRealm子类(本处是AccountRealm)的doGetAuthenticationInfo方法作为参数
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest,
ServletResponse servletResponse) {

HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(AuthConstant.AUTHENTICATION_HEADER);
// 自定义Header也可以,但浏览器不会存自定义的Header,需要前端自己去存
// String token = request.getHeader("Authentication");
if (!StringUtils.hasText(token)) {
return null;
}
return new JwtToken(token);
}
}

自定义jwt的token

package com.example.demo.config.shiro.entity;

import org.apache.shiro.authc.AuthenticationToken;

/**
* JwtToken代替官方的UsernamePasswordToken,是Shiro用户名、密码等信息的载体,
* 前后端分离,服务器不保存用户状态,所以不需要RememberMe等功能。
*/
public class JwtToken implements AuthenticationToken {

private final String token;

public JwtToken(String jwt) {
this.token = jwt;
}

@Override
public Object getPrincipal() {
return token;
}

@Override
public Object getCredentials() {
return token;
}
}

其他配置

获得配置文件中jwt的配置

配置类

package com.example.demo.config;

import com.example.demo.config.properties.JwtProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JwtConfig {
@Bean
@ConfigurationProperties(prefix = "custom.jwt")
public JwtProperties jwtProperties() {
return new JwtProperties();
}
}

属性类

package com.example.demo.config.properties;

import lombok.Data;

@Data
public class JwtProperties {
private String secret;
private long expire;
}

Knife4j配置(接口文档)

package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class Knife4jConfig {
@Bean(value = "defaultApi2")
public Docket defaultApi2() {
Docket docket=new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("我的标题")
.description("我的描述")
// .termsOfServiceUrl("http://www.xx.com/")
.contact(new Contact("daoren",
.version("1.0")
.build())
//分组名称
.groupName("all")
.select()
//指定Controller扫描路径。可以不具体到controller,它会扫描指定路径下的所有
.apis(RequestHandlerSelectors.basePackage("com.example.demo"))
.paths(PathSelectors.any())
.build();
return docket;
}
}

权限(rbac包)

用户(user)

service

package com.example.demo.rbac.user.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.user.entity.User;

public interface UserService extends IService<User> {
User getUserByUserName(String userName);
}
package com.example.demo.rbac.user.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.mapper.UserMapper;
import com.example.demo.rbac.user.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Override
public User getUserByUserName(String userName) {
return lambdaQuery().eq(User::getUserName, userName).one();
}
}

mapper

package com.example.demo.rbac.user.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.user.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {
}

entity

package com.example.demo.rbac.user.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Long id;

private String userName;

private String password;

private String salt;

}

角色(role)

service

package com.example.demo.rbac.role.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.role.entity.Role;

import java.util.Set;

public interface RoleService extends IService<Role> {
Set<String> getRolesByUserId(String userId);
}
package com.example.demo.rbac.role.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.role.entity.Role;
import com.example.demo.rbac.role.mapper.RoleMapper;
import com.example.demo.rbac.role.service.RoleService;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {

@Override
public Set<String> getRolesByUserId(String userId) {
Long id = Long.parseLong(userId);
return this.getBaseMapper().getRolesByUserId(id);
}
}

mapper

package com.example.demo.rbac.role.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.role.entity.Role;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
public interface RoleMapper extends BaseMapper<Role> {
@Select("SELECT " +
" t_role.`name` " +
"FROM " +
" t_user, " +
" t_user_role_mid, " +
" t_role " +
"WHERE " +
" t_user.`id` = #{userId} " +
" AND t_user.id = t_user_role_mid.user_id " +
" AND t_user_role_mid.role_id = t_role.id")
Set<String> getRolesByUserId(@Param("userId")Long userId);
}

entity

package com.example.demo.rbac.role.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_role")
public class Role {
@TableId(value = "id", type = IdType.AUTO)
private Long id;

private String name;

private String description;

}

权限(permission)

service 

package com.example.demo.rbac.permission.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.demo.rbac.permission.entity.Permission;

import java.util.Set;

public interface PermissionService extends IService<Permission> {
Set<String> getPermissionsByUserId(Long userId);
}
package com.example.demo.rbac.permission.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.demo.rbac.permission.entity.Permission;
import com.example.demo.rbac.permission.mapper.PermissionMapper;
import com.example.demo.rbac.permission.service.PermissionService;
import org.springframework.stereotype.Service;

import java.util.Set;

@Service
public class PermissionServiceImpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {

@Override
public Set<String> getPermissionsByUserId(Long userId) {
return this.getBaseMapper().getPermissionsByUserId(userId);
}
}

mapper 

package com.example.demo.rbac.permission.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.rbac.permission.entity.Permission;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

import java.util.Set;

@Repository
public interface PermissionMapper extends BaseMapper<Permission> {
@Select("SELECT " +
" t_permission.`name` " +
"FROM " +
" t_user, " +
" t_user_role_mid, " +
" t_role, " +
" t_role_permission_mid, " +
" t_permission " +
"WHERE " +
" t_user.`id` = #{userId} " +
" AND t_user.id = t_user_role_mid.user_id " +
" AND t_user_role_mid.role_id = t_role.id " +
" AND t_role.id = t_role_permission_mid.role_id " +
" AND t_role_permission_mid.permission_id = t_permission.id")
Set<String> getPermissionsByUserId(@Param("userId") Long userId);
}

entity

package com.example.demo.rbac.permission.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_permission")
public class Permission {
@TableId(value = "id", type = IdType.AUTO)
private Long id;

private String name;

private String description;

}

业务(business包)

登录(login)

controller

package com.example.demo.business.login.controller;

import com.example.demo.business.login.entity.LoginRequest;
import com.example.demo.business.login.entity.LoginVO;
import com.example.demo.rbac.user.entity.User;
import com.example.demo.rbac.user.service.UserService;
import com.example.demo.common.constant.AuthConstant;
import com.example.demo.common.exception.BusinessException;
import com.example.demo.common.util.auth.JwtUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletResponse;

@Api(tags = "登录")
@RestController
public class LoginController {
@Autowired
private UserService userService;

@ApiOperation("登录")
@PostMapping("login")
public LoginVO login(@RequestBody LoginRequest loginRequest, HttpServletResponse response) {
String userName = loginRequest.getUserName();
String password = loginRequest.getPassword();

User user = userService.getUserByUserName(userName);
if (user == null) {
throw new BusinessException("用户不存在");
}

String calculatedPassword = new SimpleHash(AuthConstant.ALGORITHM_TYPE,
password, user.getSalt(), AuthConstant.HASH_ITERATIONS).toString();

if (!user.getPassword().equals(calculatedPassword)) {
throw new BusinessException("用户名或密码不正确");
}

String token = JwtUtil.createToken(user.getId().toString());

response.setHeader(AuthConstant.AUTHENTICATION_HEADER, token);
// 自定义Header也可以,但浏览器不会存自定义的Header,需要前端自己去存
// response.setHeader("Authentication", token);

return fillResult(user);
}

private LoginVO fillResult(User user) {
LoginVO loginVO = new LoginVO();
loginVO.setUserId(user.getId());
loginVO.setUserName(user.getUserName());
return loginVO;
}
}

entity

package com.example.demo.business.login.entity;

import lombok.Data;

@Data
public class LoginRequest {
private String userName;
private String password;
}
package com.example.demo.business.login.entity;

import lombok.Data;

@Data
public class LoginVO {
private Long userId;
private String userName;
}

登录(logout)

package com.example.demo.business.logout;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "登出")
@RestController
public class LogoutController {

@ApiOperation("登出")
@PostMapping("logout")
public Result<Object> logout() {
SecurityUtils.getSubject().logout();
return new Result();
}
}

产品(product)

controller

package com.example.demo.business.product;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "产品")
@RestController
@RequestMapping("product")
public class ProductController {
@RequiresPermissions("product:add")
@ApiOperation(value="增加产品")
@PostMapping("add")
public Result add() {
return new Result<>().message("product:add success");
}

@RequiresPermissions("product:delete")
@ApiOperation(value="删除产品")
@PostMapping("delete")
public Result delete() {
return new Result<>().message("product:delete success");
}

@RequiresPermissions("product:edit")
@ApiOperation(value="编辑产品")
@PostMapping("edit")
public Result edit() {
return new Result<>().message("product:edit success");
}

@RequiresPermissions("product:view")
@ApiOperation(value="查看产品")
@GetMapping("view")
public Result view() {
return new Result<>().message("product:view success");
}
}

订单

package com.example.demo.business.order;

import com.example.demo.common.entity.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Api(tags = "订单")
@RestController
@RequestMapping("order")
public class OrderController {
@RequiresPermissions("order:add")
@ApiOperation(value="增加订单")
@PostMapping("add")
public Result add() {
return new Result<>().message("order:add success");
}

@RequiresRoles(value = {"admin", "orderManager"}, logical = Logical.OR)
@ApiOperation(value="删除订单")
@PostMapping("delete")
public Result delete() {
return new Result<>().message("order:delete success");
}

// 管理员或者订单管理员有权限
@RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR)
@ApiOperation(value="编辑订单")
@PostMapping("edit")
public Result edit() {
return new Result<>().message("order:edit success");
}

// 此时必须同时满足这两个注解的条件才允许访问
@RequiresPermissions("order:view")
@RequiresRoles(value = {"admin", "productManager"}, logical = Logical.OR)
@ApiOperation(value="查看订单")
@GetMapping("view")
public Result view() {
return new Result<>().message("order:view success");
}
}

公共(common包)

启动类

当然,这个没在common包,但也算个公共的东西,放这来吧。 

package com.example.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.demo.**.mapper")
public class ShiroApplication {

public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}

}

全局异常处理

package com.example.demo.common.advice;

import com.example.demo.common.entity.Result;
import com.example.demo.common.exception.BusinessException;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthenticatedException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE - 1)
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler(Exception.class)
public Result<Object> handleException(Exception e) throws Exception {
log.error(e.getMessage(), e);

// 如果某个自定义异常有@ResponseStatus注解,就继续抛出
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}

// 实际项目中应该这样写,防止用户看到详细的异常信息
// return new Result().failure().message.message("操作失败");
return new Result<>().failure().message(e.getMessage());
}

@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthenticatedException.class)
public Result<Object> handleUnauthenticatedException(Exception e) {
log.error(e.getMessage(), e);
return new Result<>().failure().message(e.getMessage());
}

@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(UnauthorizedException.class)
public Result<Object> handleUnauthorizedException(Exception e) {
log.error(e.getMessage(), e);
return new Result<>().failure().message(e.getMessage());
}

@ExceptionHandler(BusinessException.class)
public Result<Object> handleBusinessException(Exception e) throws Exception {
log.error(e.getMessage(), e);

// 如果某个自定义异常有@ResponseStatus注解,就继续抛出
if (AnnotationUtils.findAnnotation(e.getClass(), ResponseStatus.class) != null) {
throw e;
}

// 实际项目中应该这样写,防止用户看到详细的异常信息
// return new Result<>().failure().message("操作失败");
return new Result<>().failure().message(e.getMessage());
}
}

全局响应处理

package com.example.demo.common.advice;

import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.Arrays;
import java.util.List;

@Slf4j
@Order(Ordered.LOWEST_PRECEDENCE)
@ControllerAdvice
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
private List<String> KNIFE4J_URI = Arrays.asList(
"/doc.html",
"/swagger-resources",
"/swagger-resources/configuration",
"/v3/api-docs",
"/v2/api-docs");
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 若接口返回的类型本身就是ResultWrapper,则无需操作,返回false
// return !returnType.getParameterType().equals(ResultWrapper.class);
return true;
}

@Override
@ResponseBody
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
// 若返回值为String类型,需要包装为String类型返回。否则会报错
try {
ObjectMapper objectMapper = new ObjectMapper();
Result<Object> result = new Result<>().data(body);
return objectMapper.writeValueAsString(result);
} catch (JsonProcessingException e) {
throw new RuntimeException("序列化String错误");
}
} else if (body instanceof Result) {
return body;
} else if (isKnife4jUrl(request.getURI().getPath())) {
// 如果是接口文档uri,直接跳过
return body;
}
return new Result<>().data(body);
}

private boolean isKnife4jUrl(String uri) {
AntPathMatcher pathMatcher = new AntPathMatcher();
for (String s : KNIFE4J_URI) {
if (pathMatcher.match(s, uri)) {
return true;
}
}
return false;
}
}

常量

白名单 

package com.example.demo.common.constant;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public interface WhiteList {
List<String> KNIFE4J = Arrays.asList(
"/doc.html",
"/swagger-resources",
"/swagger-resources/configuration",
"/v3/api-docs",
"/v2/api-docs",
"/webjars/**");

List<String> ALL = new ArrayList<>(KNIFE4J);
}

响应数据的code 

package com.example.demo.common.constant;

public enum ResultCode {
SUCCESS(1000, "访问成功"),
SYSTEM_FAILURE(1001, "系统异常"),
;

private final int code;
private final String description;

ResultCode(int code, String description) {
this.code = code;
this.description = description;
}

public int getCode() {
return code;
}

public String getDescription() {
return description;
}
}

授权相关

package com.example.demo.common.constant;

import org.springframework.http.HttpHeaders;

public interface AuthConstant {
String ALGORITHM_TYPE = "md5";
int HASH_ITERATIONS = 2;
String AUTHENTICATION_HEADER = HttpHeaders.COOKIE;
}

实体类

封装响应结果 

package com.example.demo.common.entity;

import com.example.demo.common.constant.ResultCode;
import lombok.Data;

@Data
public class Result<T> {
private boolean success = true;

private int code = ResultCode.SUCCESS.getCode();

private String message;

private T data;

public Result() {
}

public Result(boolean success) {
this.success = success;
}

public Result<T> success(boolean success) {
Result<T> result = new Result<>(success);
if (success) {
result.code = ResultCode.SUCCESS.getCode();
} else {
result.code = ResultCode.SYSTEM_FAILURE.getCode();
}
return result;
}

public Result<T> success() {
return success(true);
}

public Result<T> failure() {
return success(false);
}

/**
* @param code {@link ResultCode#getCode()}
*/
public Result<T> code(int code) {
this.code = code;
return this;
}

public Result<T> message(String message) {
this.message = message;
return this;
}

public Result<T> data(T data) {
this.data = data;
return this;
}
}

账户信息

package com.example.demo.common.entity;

import com.example.demo.common.util.auth.ShiroUtil;
import lombok.Data;

/**
* 存放账户的信息。
* 在登录后会实例化一个此对象,然后放到subject里边。
* 获取方法:{@link ShiroUtil#getProfile()}
*/
@Data
public class AccountProfile {
private String id;
private String userName;
}

异常

自定义异常(业务异常) 

package com.example.demo.common.exception;

public class BusinessException extends RuntimeException{
public BusinessException() {
super();
}

public BusinessException(String message) {
super(message);
}

public BusinessException(String message, Throwable cause) {
super(message, cause);
}
}

工具类

 Spring的ApplicationContext的持有者

package com.example.demo.common.util;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;

public void setApplicationContext(ApplicationContext context)
throws BeansException {
ApplicationContextHolder.context = context;
}

public static ApplicationContext getContext() {
return context;
}
}

jwt工具类

package com.example.demo.common.util.auth;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.example.demo.common.util.ApplicationContextHolder;
import com.example.demo.config.properties.JwtProperties;

import java.util.Date;

public class JwtUtil {
private static final JwtProperties jwtProperties;

static {
jwtProperties = ApplicationContextHolder.getContext().getBean(JwtProperties.class);
}

// 创建jwt token
public static String createToken(String userId) {
try {
Date date = new Date(System.currentTimeMillis() + jwtProperties.getExpire() * 1000);
Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret());
return JWT.create()
// 自定义私有的payload的key-value。比如:.withClaim("userName", "Tony")
// .withClaim("key1", "value1")
.withAudience(userId) // 将 user id 保存到 token 里面
.withExpiresAt(date) // date之后,token过期
.sign(algorithm); // token 的密钥
} catch (Exception e) {
return null;
}
}

/**
* 校验token
* 若校验失败,会抛出异常:{@link JWTVerificationException}
* 失败情况(按先后顺序):
* - 算法不匹配:{@link com.auth0.jwt.exceptions.AlgorithmMismatchException}
* - 签名验证失败:{@link com.auth0.jwt.exceptions.SignatureVerificationException}
* - Claim无效:{@link com.auth0.jwt.exceptions.InvalidClaimException}
* - token超期:{@link com.auth0.jwt.exceptions.TokenExpiredException}
*/
public static void verifyToken(String token) {
Algorithm algorithm = Algorithm.HMAC512(jwtProperties.getSecret());

JWTVerifier jwtVerifier = JWT.require(algorithm)
// .withIssuer("auth0")
// .withClaim("userName", userName)
.build();

DecodedJWT jwt = jwtVerifier.verify(token);
}

public static String getUserIdByToken(String token) {
try {
return JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException e) {
return null;
}
}

public static boolean isTokenExpired(String token) {
DecodedJWT decodedJWT = JWT.decode(token);
return decodedJWT.getExpiresAt().before(new Date());
}

}

Shiro工具类

package com.example.demo.common.util.auth;

import com.example.demo.common.entity.AccountProfile;
import org.apache.shiro.SecurityUtils;

public class ShiroUtil {

// 用于获得当前账户的信息。
public static AccountProfile getProfile() {
return (AccountProfile) SecurityUtils.getSubject().getPrincipal();
}

}

响应工具类

package com.example.demo.common.util;

import com.example.demo.common.entity.Result;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpStatus;

import javax.servlet.http.HttpServletResponse;

public class ResponseUtil {
public static void jsonResponse(HttpServletResponse response, int status, String message) throws Exception {
//让浏览器用utf8来解析返回的数据
response.setHeader("Content-type", "application/json;charset=UTF-8");
//告诉servlet用UTF-8转码,而不是用默认的ISO8859
response.setCharacterEncoding("UTF-8");
response.setStatus(status);

Result result = new Result().failure().message(message);
String json = new ObjectMapper().writeValueAsString(result);
response.getWriter().print(json);
}
}

测试

测试超级管理员(admin)

启动项目,访问:​​http://localhost:8080/doc.html​

Shiro--整合jwt--使用/教程/实例_java_03

1.测试登录


  1. 登录成功
  2. 可以看到,会返回来一个Set-Cookie头,值是token。

Shiro--整合jwt--使用/教程/实例_apache_04

2.测试有资源权限的接口

本处测试增加产品接口。


  1. 成功访问。
  2. 在请求时会传递Cookie

Shiro--整合jwt--使用/教程/实例_java_05

我使用标准的:Set-Cookie,Cookie来做认证的。若是自定义的header,需要手动写入:

Shiro--整合jwt--使用/教程/实例_spring_06

3.测试登出

Shiro--整合jwt--使用/教程/实例_spring_07

4.再次访问接口


  1. 访问成功。
  2.  因为token还没过期,浏览器也还会将其发给服务端,所以成功。

Shiro--整合jwt--使用/教程/实例_spring boot_08

测试产品管理员(productManager)

启动项目,访问:​​http://localhost:8080/doc.html​

Shiro--整合jwt--使用/教程/实例_spring boot_09

1.测试登录


  1. 登录成功
  2. 可以看到,会返回来一个Set-Cookie头,值是token。

Shiro--整合jwt--使用/教程/实例_spring boot_10

2.测试有资源权限的接口

本处测试增加产品接口。


  1. 成功访问。
  2. 在请求时会传递Cookie

Shiro--整合jwt--使用/教程/实例_spring boot_11

3.测试无资源权限的接口

本处测试增加订单接口。


  1. 访问失败。
  2. 在请求时会传递Cookie
  3. 有一处细节:提示是红色的。这就是@ResponseStatus的作用

Shiro--整合jwt--使用/教程/实例_spring boot_12

点进去看,可以看到状态码是我指定的:403

Shiro--整合jwt--使用/教程/实例_apache_13

4.测试有角色权限的接口

本处测试编辑订单接口。


  1. 访问成功。
  2. 在请求时会传递Cookie

Shiro--整合jwt--使用/教程/实例_spring boot_14

4.测试无角色权限的接口

本处测试删除订单接口。


  1. 访问失败。
  2. 在请求时会传递Cookie

Shiro--整合jwt--使用/教程/实例_java_15

Shiro--整合jwt--使用/教程/实例_apache_16

测试同时有角色与权限注解

1.登录(productManager)

Shiro--整合jwt--使用/教程/实例_spring boot_17

2.请求同时有角色和资源权限注解的接口

本处请求订单查看接口。


  1. 请求失败。
  2. 提示无资源权限。

由此推断,如果有两个注解,则必须同时满足这两个注解才可以。

Shiro--整合jwt--使用/教程/实例_apache_18

3.登录(admin)

使用admin角色尝试,此角色同时有所有的资源权限。

Shiro--整合jwt--使用/教程/实例_spring_19

4.请求同时有角色和资源权限注解的接口

本处请求订单查看接口。

  1. 请求成功

说明如果有两个注解,则必须同时满足这两个注解才可以。

Shiro--整合jwt--使用/教程/实例_spring boot_20

重启服务再请求

1.登录

登录成功 

Shiro--整合jwt--使用/教程/实例_spring_21

2.重启服务器

重启Idea启动的应用。

3.访问有权限的接口

本处访问产品增加接口。

  1. 可以看到,访问成功。

Shiro--整合jwt--使用/教程/实例_apache_22

超时后再请求

1.修改配置文件,暂时将token过期时间改短(本处改为10秒)

application.yml

Shiro--整合jwt--使用/教程/实例_spring_23

2.登录

Shiro--整合jwt--使用/教程/实例_java_24

3.等待大于10秒之后再请求


请求失败。

Shiro--整合jwt--使用/教程/实例_spring boot_25

我代码里指定这种错误为401,点进去验证下:

Shiro--整合jwt--使用/教程/实例_spring_26



其他网址

超详细!4小时开发一个SpringBoot+vue前后端分离博客项目!!

SpringBoot 集成 JWT 和 Apache Shiro - 知乎