目录
第六步:实现自定义的AuthenticationToken。
第八步:实现自定义AuthenticatingFilter。
前言
前段日子写过一篇关于SpringBoot+Shiro的简单整合的例子,那个例子并不适用于我们目前的前后端分离开发的趋势。我之前写过一个项目也是用到了Shiro的前后端分离,某度了许久也没找到解决方案,什么去掉shiroFilter.setLoginUrl();也阻止不了讨人厌的login.jsp的出现。直到我看到了renren-fast的源码...废话不多说,让我们来看看如何实现吧!
前后端分离
要实现前后端分离,需要考虑以下2个问题: 1. 项目不再基于session了,如何知道访问者是谁? 2. 如何确认访问者的权限?
前后端分离,一般都是通过token实现,本项目也是一样;用户登录时,生成token及 token过期时间,token与用户是一一对应关系,调用接口的时候,把token放到header或 请求参数中,服务端就知道是谁在调用接口。
代码已上传到Git:
后台代码:https://github.com/FENGZHIJIE1998/shiro-auth
前端代码:https://github.com/FENGZHIJIE1998/shiro-vue
觉得好用的记得点个Star哦
Let's do it!!
介绍:这次我们使用Shiro快速搭建前后端分离的权限管理系统 利用JPA帮我们管理数据库,Swagger Knife4j 帮我搭建Web测试环境;
后台基于 Springboot JPA Knife4j Shiro
前端基于 VUE ElementUI
注意:主要观察token的使用方法!
第一步:新建工程
pom文件application.yml巴拉巴拉这里省略,这里贴出需要用到的依赖:
-
<!--starter-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter
</artifactId>
-
</dependency>
-
<!-- test-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-test
</artifactId>
-
<scope>test
</scope>
-
</dependency>
-
<!--web-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-web
</artifactId>
-
</dependency>
-
<!--validation-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-validation
</artifactId>
-
</dependency>
-
<!--JPA-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-data-jpa
</artifactId>
-
</dependency>
-
<!--JDBC-->
-
<dependency>
-
<groupId>org.springframework.boot
</groupId>
-
<artifactId>spring-boot-starter-jdbc
</artifactId>
-
</dependency>
-
<!--lombok-->
-
<dependency>
-
<groupId>org.projectlombok
</groupId>
-
<artifactId>lombok
</artifactId>
-
</dependency>
-
<!-- shiro-->
-
<dependency>
-
<groupId>org.apache.shiro
</groupId>
-
<artifactId>shiro-spring
</artifactId>
-
<version>1.3.2
</version>
-
</dependency>
-
<!--mysql-connector-->
-
<dependency>
-
<groupId>mysql
</groupId>
-
<artifactId>mysql-connector-java
</artifactId>
-
<scope>runtime
</scope>
-
</dependency>
-
<!-- druid-spring-boot-starter -->
-
<dependency>
-
<groupId>com.alibaba
</groupId>
-
<artifactId>druid-spring-boot-starter
</artifactId>
-
<version>1.1.10
</version>
-
</dependency>
-
<!-- swagger -->
-
<dependency>
-
<groupId>com.spring4all
</groupId>
-
<artifactId>swagger-spring-boot-starter
</artifactId>
-
<version>1.8.0.RELEASE
</version>
-
</dependency>
-
<!-- knife4j -->
-
<dependency>
-
<groupId>com.github.xiaoymin
</groupId>
-
<artifactId>knife4j-spring-boot-starter
</artifactId>
-
<version>2.0.2
</version>
-
</dependency>
-
<!-- commons-lang -->
-
<dependency>
-
<groupId>commons-lang
</groupId>
-
<artifactId>commons-lang
</artifactId>
-
<version>2.6
</version>
-
</dependency>
第二步:准备好要用的包包和类类
第三步:编写登陆入口
为了方便这里不做密码加盐加密:
-
/**
-
* @Author 大誌
-
* @Date 2019/3/30 22:04
-
* @Version 1.0
-
*/
-
@RestController
-
public
class ShiroController {
-
-
private
final ShiroService shiroService;
-
-
public ShiroController(ShiroService shiroService) {
-
this.shiroService = shiroService;
-
}
-
-
-
/**
-
* 登录
-
*/
-
@ApiOperation(value = "登陆", notes = "参数:用户名 密码")
-
@PostMapping("/sys/login")
-
public Map<String, Object> login(@RequestBody @Validated LoginDTO loginDTO, BindingResult bindingResult) {
-
Map<String, Object> result =
new HashMap<>();
-
if (bindingResult.hasErrors()) {
-
result.put(
"status",
400);
-
result.put(
"msg", bindingResult.getFieldError().getDefaultMessage());
-
return result;
-
}
-
-
String username = loginDTO.getUsername();
-
String password = loginDTO.getPassword();
-
//用户信息
-
User user = shiroService.findByUsername(username);
-
//账号不存在、密码错误
-
if (user ==
null || !user.getPassword().equals(password)) {
-
result.put(
"status",
400);
-
result.put(
"msg",
"账号或密码有误");
-
}
else {
-
//生成token,并保存到数据库
-
result = shiroService.createToken(user.getUserId());
-
result.put(
"status",
200);
-
result.put(
"msg",
"登陆成功");
-
}
-
return result;
-
}
-
-
/**
-
* 退出
-
*/
-
@ApiOperation(value = "登出", notes = "参数:token")
-
@PostMapping("/sys/logout")
-
public Map<String, Object> logout(@RequestHeader("token")String token) {
-
Map<String, Object> result =
new HashMap<>();
-
shiroService.logout(token);
-
result.put(
"status",
200);
-
result.put(
"msg",
"您已安全退出系统");
-
return result;
-
}
-
}
第四步:编写ShiroService中的方法
主要是生成一个token返回给前端。
-
/**
-
* @Author 大誌
-
* @Date 2019/3/30 22:18
-
* @Version 1.0
-
*/
-
@Service
-
public
class ShiroServiceImpl implements ShiroService {
-
-
-
@Autowired
-
private UserRepository userRepository;
-
@Autowired
-
private SysTokenRepository sysTokenRepository;
-
-
/**
-
* 根据username查找用户
-
*
-
* @param username
-
* @return User
-
*/
-
@Override
-
public User findByUsername(String username) {
-
User user = userRepository.findByUsername(username);
-
return user;
-
}
-
-
//12小时后过期
-
private
final
static
int EXPIRE =
3600 *
12;
-
-
@Override
-
/**
-
* 生成一个token
-
*@param [userId]
-
*@return Result
-
*/
-
public Map<String, Object> createToken(Integer userId) {
-
Map<String, Object> result =
new HashMap<>();
-
//生成一个token
-
String token = TokenGenerator.generateValue();
-
//当前时间
-
Date now =
new Date();
-
//过期时间
-
Date expireTime =
new Date(now.getTime() + EXPIRE *
1000);
-
//判断是否生成过token
-
SysToken tokenEntity = sysTokenRepository.findByUserId(userId);
-
if (tokenEntity ==
null) {
-
tokenEntity =
new SysToken();
-
tokenEntity.setUserId(userId);
-
tokenEntity.setToken(token);
-
tokenEntity.setUpdateTime(now);
-
tokenEntity.setExpireTime(expireTime);
-
//保存token
-
sysTokenRepository.save(tokenEntity);
-
}
else {
-
tokenEntity.setToken(token);
-
tokenEntity.setUpdateTime(now);
-
tokenEntity.setExpireTime(expireTime);
-
//更新token
-
sysTokenRepository.save(tokenEntity);
-
}
-
result.put(
"token", token);
-
result.put(
"expire", EXPIRE);
-
return result;
-
}
-
-
@Override
-
public void logout(String token) {
-
SysToken byToken = findByToken(token);
-
//生成一个token
-
token = TokenGenerator.generateValue();
-
//修改token
-
SysToken tokenEntity =
new SysToken();
-
tokenEntity.setUserId(byToken.getUserId());
-
tokenEntity.setToken(token);
-
sysTokenRepository.save(tokenEntity);
-
}
-
-
@Override
-
public SysToken findByToken(String accessToken) {
-
return sysTokenRepository.findByToken(accessToken);
-
-
}
-
-
@Override
-
public User findByUserId(Integer userId) {
-
return userRepository.findByUserId(userId);
-
}
-
}
第五步:编写ShiroConfig类
-
/**
-
* @Author 大誌
-
* @Date 2019/3/30 21:50
-
* @Version 1.0
-
*/
-
@Configuration
-
public
class ShiroConfig {
-
-
@Bean("securityManager")
-
public SecurityManager securityManager(AuthRealm authRealm) {
-
DefaultWebSecurityManager securityManager =
new DefaultWebSecurityManager();
-
securityManager.setRealm(authRealm);
-
securityManager.setRememberMeManager(
null);
-
return securityManager;
-
}
-
-
@Bean("shiroFilter")
-
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
-
ShiroFilterFactoryBean shiroFilter =
new ShiroFilterFactoryBean();
-
shiroFilter.setSecurityManager(securityManager);
-
//oauth过滤
-
Map<String, Filter> filters =
new HashMap<>();
-
filters.put(
"auth",
new AuthFilter());
-
shiroFilter.setFilters(filters);
-
Map<String, String> filterMap =
new LinkedHashMap<>();
-
filterMap.put(
"/webjars/**",
"anon");
-
filterMap.put(
"/druid/**",
"anon");
-
filterMap.put(
"/sys/login",
"anon");
-
filterMap.put(
"/swagger/**",
"anon");
-
filterMap.put(
"/v2/api-docs",
"anon");
-
filterMap.put(
"/swagger-ui.html",
"anon");
-
filterMap.put(
"/swagger-resources/**",
"anon");
-
filterMap.put(
"/**",
"auth");
-
shiroFilter.setFilterChainDefinitionMap(filterMap);
-
-
return shiroFilter;
-
}
-
-
@Bean("lifecycleBeanPostProcessor")
-
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
-
return
new LifecycleBeanPostProcessor();
-
}
-
-
@Bean
-
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
-
AuthorizationAttributeSourceAdvisor advisor =
new AuthorizationAttributeSourceAdvisor();
-
advisor.setSecurityManager(securityManager);
-
return advisor;
-
}
-
}
第六步:实现自定义的AuthenticationToken。
阅读AuthenticatingFilter抽象类中executeLogin方法,我们发现调用 了subject.login(token),这是shiro的登录方法,且需要token参数,我们自定义 AuthToken类,只要实现AuthenticationToken接口,就可以了。
-
//AuthenticatingFilter中的executeLogin()
-
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
-
AuthenticationToken token = createToken(request, response);
-
if (token ==
null) {
-
String msg =
"createToken method implementation returned null. A valid non-null AuthenticationToken " +
-
"must be created in order to execute a login attempt.";
-
throw
new IllegalStateException(msg);
-
}
-
try {
-
Subject subject = getSubject(request, response);
-
//重点!
-
subject.login(token);
-
return onLoginSuccess(token, subject, request, response);
-
}
catch (AuthenticationException e) {
-
return onLoginFailure(token, e, request, response);
-
}
-
}
-
/**
-
* 自定义AuthenticationToken类
-
* @Author 大誌
-
* @Date 2019/3/31 10:58
-
* @Version 1.0
-
*/
-
public
class AuthToken extends UsernamePasswordToken{
-
-
private String token;
-
-
public AuthToken(String token) {
-
this.token = token;
-
}
-
-
@Override
-
public Object getPrincipal() {
-
return token;
-
}
-
-
@Override
-
public Object getCredentials() {
-
return token;
-
}
-
}
这里我实现的时候出现了Token不匹配的Bug。DeBug下可以查到源头是代码是用UsernamePasswordToken.class和我自定义的AuthToken.class配对。按道理应该是true,却返回了false...于是我就把自定义的AuthToken不实现AuthenticationToken,转为继承UsernamePasswordToken,就可以了。(renren-fast中却可以,可能是版本的问题)
2020/4/27修改: 为了避免误导,将上诉代码 AuthenticationToken 修改为 UsernamePasswordToken,并且走了一下源码,发现这个getAuthenticationTokenClass()实际上获取到的是UsernamePasswordToken.class
再回头看看renren-fast中的源码,原来他重写了supports方法!
第七步:编写自己的Realm
发起请求时,接受传过来的token后,如何保证token有效及用户权限呢?调用接口时,接受传过来的token后,如何保证token有效及用户权限呢?其实,Shiro提供了AuthorizingRealm以及AuthenticatingFilter抽象类,继承AuthorizingRealm和AuthenticatingFilter抽象类重写方法即可。
-
/**
-
* @Author 大誌
-
* @Date 2019/3/30 21:38
-
* @Version 1.0
-
*/
-
@Component
-
public
class AuthRealm extends AuthorizingRealm {
-
-
@Autowired
-
private ShiroService shiroService;
-
-
@Override
-
/**
-
* 授权 获取用户的角色和权限
-
*@param [principals]
-
*@return org.apache.shiro.authz.AuthorizationInfo
-
*/
-
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
-
//1. 从 PrincipalCollection 中来获取登录用户的信息
-
User user = (User) principals.getPrimaryPrincipal();
-
//Integer userId = user.getUserId();
-
//2.添加角色和权限
-
SimpleAuthorizationInfo simpleAuthorizationInfo =
new SimpleAuthorizationInfo();
-
for (Role role : user.getRoles()) {
-
//2.1添加角色
-
simpleAuthorizationInfo.addRole(role.getRoleName());
-
for (Permission permission : role.getPermissions()) {
-
//2.1.1添加权限
-
simpleAuthorizationInfo.addStringPermission(permission.getPermission());
-
}
-
}
-
return simpleAuthorizationInfo;
-
}
-
-
@Override
-
/**
-
* 认证 判断token的有效性
-
*@param [token]
-
*@return org.apache.shiro.authc.AuthenticationInfo
-
*/
-
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
-
//获取token,既前端传入的token
-
String accessToken = (String) token.getPrincipal();
-
//1. 根据accessToken,查询用户信息
-
SysToken tokenEntity = shiroService.findByToken(accessToken);
-
//2. token失效
-
if (tokenEntity ==
null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
-
throw
new IncorrectCredentialsException(
"token失效,请重新登录");
-
}
-
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
-
User user = shiroService.findByUserId(tokenEntity.getUserId());
-
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
-
if (user ==
null) {
-
throw
new UnknownAccountException(
"用户不存在!");
-
}
-
//5. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
-
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(user, accessToken,
this.getName());
-
return info;
-
}
-
}
第八步:实现自定义AuthenticatingFilter。
-
/**
-
* Shiro自定义auth过滤器
-
*
-
* @Author 大誌
-
* @Date 2019/3/31 10:38
-
* @Version 1.0
-
*/
-
@Component
-
public
class AuthFilter extends AuthenticatingFilter {
-
-
-
// 定义jackson对象
-
private
static
final ObjectMapper MAPPER =
new ObjectMapper();
-
-
/**
-
* 生成自定义token
-
*
-
* @param request
-
* @param response
-
* @return
-
* @throws Exception
-
*/
-
@Override
-
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
-
//获取请求token
-
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
-
-
return
new AuthToken(token);
-
}
-
-
/**
-
* 步骤1.所有请求全部拒绝访问
-
*
-
* @param request
-
* @param response
-
* @param mappedValue
-
* @return
-
*/
-
@Override
-
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
-
if (((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())) {
-
return
true;
-
}
-
return
false;
-
}
-
-
/**
-
* 步骤2,拒绝访问的请求,会调用onAccessDenied方法,onAccessDenied方法先获取 token,再调用executeLogin方法
-
*
-
* @param request
-
* @param response
-
* @return
-
* @throws Exception
-
*/
-
@Override
-
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
-
//获取请求token,如果token不存在,直接返回
-
String token = TokenUtil.getRequestToken((HttpServletRequest) request);
-
if (StringUtils.isBlank(token)) {
-
HttpServletResponse httpResponse = (HttpServletResponse) response;
-
httpResponse.setHeader(
"Access-Control-Allow-Credentials",
"true");
-
httpResponse.setHeader(
"Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
-
httpResponse.setCharacterEncoding(
"UTF-8");
-
Map<String, Object> result =
new HashMap<>();
-
result.put(
"status",
400);
-
result.put(
"msg",
"请先登录");
-
String json = MAPPER.writeValueAsString(result);
-
httpResponse.getWriter().print(json);
-
return
false;
-
}
-
return executeLogin(request, response);
-
}
-
-
/**
-
* token失效时候调用
-
*/
-
@Override
-
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
-
HttpServletResponse httpResponse = (HttpServletResponse) response;
-
httpResponse.setContentType(
"application/json;charset=utf-8");
-
httpResponse.setHeader(
"Access-Control-Allow-Credentials",
"true");
-
httpResponse.setHeader(
"Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
-
httpResponse.setCharacterEncoding(
"UTF-8");
-
try {
-
//处理登录失败的异常
-
Throwable throwable = e.getCause() ==
null ? e : e.getCause();
-
Map<String, Object> result =
new HashMap<>();
-
result.put(
"status",
400);
-
result.put(
"msg",
"登录凭证已失效,请重新登录");
-
String json = MAPPER.writeValueAsString(result);
-
httpResponse.getWriter().print(json);
-
}
catch (IOException e1) {
-
}
-
return
false;
-
}
-
-
}
第九步:详解校验流程
先给你们上一个超级详细的流程图。
接着我们打上断点按照代码走走,可能会有点啰嗦。
1. 前端发起请求首先会进入AuthFilter的 isAccessAllowed(),除了OPTION方法,其余都拦截。
2. 拦截之后进入AuthFilter的onAccessDenied(),这里获取token后判断token是否isBlank。如果是,代表请求未携带token,直接默认返回400,未登录给前端,流程就结束了。如果携带了token则进入第三步,继续流程。
3. 接着进入AuthFilter的createToken,这里生成我们自定义的AuthToken对象。
4. 接着就会来到AuthRealm中的doGetAuthenticationInfo(),在这个方法中继续token的有效性校验,例如过期、和数据库的token对不上(用户已退出)的情况。如果校验失败,进入第5步,否则进入第6步。
5. token失效后回到AuthFilter中的onLoginFailure(),返回400以及msg,流程结束。
6. Token校验成功后进入AuthRealm的doGetAuthorizationInfo(),进行获取当前用户拥有的权限,之后底层代码会进行权限验证。如果用户有权限则会进入请求方法,否则抛出异常。到这一步校验过程就结束了。
看看效果
终于熬完上面的步骤了,这时候总体的架构已经确立好了,下面让我们来看看效果如何
DTO
-
/**
-
* 登录传输类
-
*/
-
@Data
-
public
class LoginDTO {
-
@NotBlank(message = "用户名不能为空")
-
private String username;
-
@NotBlank(message = "密码不能为空")
-
private String password;
-
}
实体类
-
@Getter
-
@Setter
-
@Entity
-
public
class User {
-
@Id
-
private Integer userId;
-
-
private String username;
-
private String password;
-
-
@ManyToMany(fetch = FetchType.EAGER)
-
@JoinTable(name = "user_role",
-
joinColumns = {@JoinColumn(name = "USER_ID", referencedColumnName = "userId")},
-
inverseJoinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")})
-
private Set<Role> roles;
-
-
}
-
-
@Getter
-
@Setter
-
@Entity
-
public
class Role {
-
-
@Id
-
private Integer roleId;
-
private String roleName;
-
-
@ManyToMany(fetch = FetchType.EAGER)
-
@JoinTable(name = "role_permission",
-
joinColumns = {@JoinColumn(name = "ROLE_ID", referencedColumnName = "roleId")},
-
inverseJoinColumns = {@JoinColumn(name = "PERMISSION_ID", referencedColumnName = "permissionId")})
-
private Set<Permission> permissions;
-
}
-
-
@Getter
-
@Setter
-
@Entity
-
public
class Permission {
-
-
@Id
-
private Integer permissionId;
-
private String permissionName;
-
private String permission;
-
}
-
-
@Getter
-
@Setter
-
@Entity
-
public
class SysToken{
-
-
@Id
-
private Integer userId;
-
private String token;
-
private Date expireTime;
-
private Date updateTime
-
}
以及给实体类附上权限:
我定义了三个用户
用户 | 角色 | 权限 |
Jack | SVIP | select;save;delete;update |
Rose | VIP | select;save;update |
Paul | P | select |
-
/*
-
Navicat MySQL Data Transfer
-
Source Server : localhost
-
Source Server Version : 50549
-
Source Host : localhost:3306
-
Source Database : shiro
-
Target Server Type : MYSQL
-
Target Server Version : 50549
-
File Encoding : 65001
-
Date: 2019-04-07 17:06:36
-
*/
-
-
SET FOREIGN_KEY_CHECKS=
0;
-
-
-- ----------------------------
-
-- Table structure for permission
-
-- ----------------------------
-
DROP
TABLE
IF
EXISTS
`permission`;
-
CREATE
TABLE
`permission` (
-
`permission_id`
int(
11)
NOT
NULL,
-
`permission`
varchar(
255)
DEFAULT
NULL,
-
`permission_name`
varchar(
255)
DEFAULT
NULL,
-
PRIMARY
KEY (
`permission_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-- ----------------------------
-
-- Records of permission
-
-- ----------------------------
-
INSERT
INTO
`permission`
VALUES (
'1',
'select',
'查看');
-
INSERT
INTO
`permission`
VALUES (
'2',
'update',
'更新');
-
INSERT
INTO
`permission`
VALUES (
'3',
'delete',
'删除');
-
INSERT
INTO
`permission`
VALUES (
'4',
'save',
'新增');
-
-
-- ----------------------------
-
-- Table structure for role
-
-- ----------------------------
-
DROP
TABLE
IF
EXISTS
`role`;
-
CREATE
TABLE
`role` (
-
`role_id`
int(
11)
NOT
NULL,
-
`role_name`
varchar(
255)
DEFAULT
NULL,
-
PRIMARY
KEY (
`role_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-- ----------------------------
-
-- Records of role
-
-- ----------------------------
-
INSERT
INTO
`role`
VALUES (
'1',
'svip');
-
INSERT
INTO
`role`
VALUES (
'2',
'vip');
-
INSERT
INTO
`role`
VALUES (
'3',
'p');
-
-
-- ----------------------------
-
-- Table structure for role_permission
-
-- ----------------------------
-
DROP
TABLE
IF
EXISTS
`role_permission`;
-
CREATE
TABLE
`role_permission` (
-
`role_id`
int(
11)
NOT
NULL,
-
`permission_id`
int(
11)
NOT
NULL,
-
PRIMARY
KEY (
`role_id`,
`permission_id`),
-
KEY
`FKf8yllw1ecvwqy3ehyxawqa1qp` (
`permission_id`),
-
CONSTRAINT
`FKa6jx8n8xkesmjmv6jqug6bg68`
FOREIGN
KEY (
`role_id`)
REFERENCES
`role` (
`role_id`),
-
CONSTRAINT
`FKf8yllw1ecvwqy3ehyxawqa1qp`
FOREIGN
KEY (
`permission_id`)
REFERENCES
`permission` (
`permission_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-- ----------------------------
-
-- Records of role_permission
-
-- ----------------------------
-
INSERT
INTO
`role_permission`
VALUES (
'1',
'1');
-
INSERT
INTO
`role_permission`
VALUES (
'2',
'1');
-
INSERT
INTO
`role_permission`
VALUES (
'3',
'1');
-
INSERT
INTO
`role_permission`
VALUES (
'1',
'2');
-
INSERT
INTO
`role_permission`
VALUES (
'2',
'2');
-
INSERT
INTO
`role_permission`
VALUES (
'1',
'3');
-
INSERT
INTO
`role_permission`
VALUES (
'1',
'4');
-
INSERT
INTO
`role_permission`
VALUES (
'2',
'4');
-
-
-- ----------------------------
-
-- Table structure for user
-
-- ----------------------------
-
DROP
TABLE
IF
EXISTS
`user`;
-
CREATE
TABLE
`user` (
-
`user_id`
int(
11)
NOT
NULL,
-
`password`
varchar(
255)
DEFAULT
NULL,
-
`username`
varchar(
255)
DEFAULT
NULL,
-
PRIMARY
KEY (
`user_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-- ----------------------------
-
-- Records of user
-
-- ----------------------------
-
INSERT
INTO
`user`
VALUES (
'1',
'123',
'Jack');
-
INSERT
INTO
`user`
VALUES (
'2',
'123',
'Rose');
-
INSERT
INTO
`user`
VALUES (
'3',
'123',
'Paul');
-
-
-- ----------------------------
-
-- Table structure for user_role
-
-- ----------------------------
-
DROP
TABLE
IF
EXISTS
`user_role`;
-
CREATE
TABLE
`user_role` (
-
`user_id`
int(
11)
NOT
NULL,
-
`role_id`
int(
11)
NOT
NULL,
-
PRIMARY
KEY (
`user_id`,
`role_id`),
-
KEY
`FKa68196081fvovjhkek5m97n3y` (
`role_id`),
-
CONSTRAINT
`FK859n2jvi8ivhui0rl0esws6o`
FOREIGN
KEY (
`user_id`)
REFERENCES
`user` (
`user_id`),
-
CONSTRAINT
`FKa68196081fvovjhkek5m97n3y`
FOREIGN
KEY (
`role_id`)
REFERENCES
`role` (
`role_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-- ----------------------------
-
-- Table structure for sys_token
-
-- ----------------------------
-
CREATE
TABLE
`sys_token` (
-
`user_id`
int(
11)
NOT
NULL,
-
`expire_time` datetime
DEFAULT
NULL,
-
`token`
varchar(
255)
DEFAULT
NULL,
-
`update_time` datetime
DEFAULT
NULL,
-
PRIMARY
KEY (
`user_id`)
-
)
ENGINE=
InnoDB
DEFAULT
CHARSET=utf8;
-
-
-
-
-- ----------------------------
-
-- Records of user_role
-
-- ----------------------------
-
INSERT
INTO
`user_role`
VALUES (
'1',
'1');
-
INSERT
INTO
`user_role`
VALUES (
'2',
'2');
-
INSERT
INTO
`user_role`
VALUES (
'3',
'3');
测试类:因为我是用Swagger来测试,所以为了方便就直接传递token参数。具体开发时候可由前端把接收到的token放入Header。
-
/**
-
* @Author 大誌
-
* @Date 2019/4/7 15:20
-
* @Version 1.0
-
*/
-
@RestController("/test")
-
public
class TestController {
-
-
@RequiresPermissions({"save"})
//没有的话 AuthorizationException
-
@PostMapping("/save")
-
public Map<String, Object> save(String token) {
-
System.out.println(
"save");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有save的权力");
-
return map;
-
}
-
-
@RequiresPermissions({"delete"})
//没有的话 AuthorizationException
-
@DeleteMapping("/delete")
-
public Map<String, Object> delete(String token) {
-
System.out.println(
"delete");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有delete的权力");
-
return map;
-
}
-
-
@RequiresPermissions({"update"})
//没有的话 AuthorizationException
-
@PutMapping("update")
-
public Map<String, Object> update(String token) {
-
System.out.println(
"update");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有update的权力");
-
return map;
-
}
-
-
@RequiresPermissions({"select"})
//没有的话 AuthorizationException
-
@GetMapping("select")
-
public Map<String, Object> select(String token, HttpSession session) {
-
System.out.println(
"select");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有select的权力");
-
return map;
-
}
-
-
@RequiresRoles({"vip"})
//没有的话 AuthorizationException
-
@GetMapping("/vip")
-
public Map<String, Object> vip(String token) {
-
System.out.println(
"vip");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有VIP角色");
-
return map;
-
}
-
@RequiresRoles({"svip"})
//没有的话 AuthorizationException
-
@GetMapping("/svip")
-
public Map<String, Object> svip(String token) {
-
System.out.println(
"svip");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有SVIP角色");
-
return map;
-
}
-
@RequiresRoles({"p"})
//没有的话 AuthorizationException
-
@GetMapping("/p")
-
public Map<String, Object> p(String token) {
-
System.out.println(
"p");
-
Map<String, Object> map =
new HashMap<String, Object>();
-
map.put(
"status",
200);
-
map.put(
"msg",
"当前用户有P角色");
-
return map;
-
}
-
}
ExceptionHandler 异常处理器,用于捕获无权限时候的异常。
-
@ControllerAdvice
-
public
class MyExceptionHandler {
-
-
@ExceptionHandler(value = AuthorizationException.class)
-
@ResponseBody
-
public Map<String, String> handleException(AuthorizationException e) {
-
//e.printStackTrace();
-
Map<String, String> result =
new HashMap<String, String>();
-
result.put(
"status",
"400");
-
//获取错误中中括号的内容
-
String message = e.getMessage();
-
String msg=message.substring(message.indexOf(
"[")+
1,message.indexOf(
"]"));
-
//判断是角色错误还是权限错误
-
if (message.contains(
"role")) {
-
result.put(
"msg",
"对不起,您没有" + msg +
"角色");
-
}
else
if (message.contains(
"permission")) {
-
result.put(
"msg",
"对不起,您没有" + msg +
"权限");
-
}
else {
-
result.put(
"msg",
"对不起,您的权限有误");
-
}
-
return result;
-
}
-
}
启动项目来看看效果: 访问 localhost:9090/shiro/doc.html
登陆失败:
登陆成功:
登录成功后会返回token,记得带上token访问以下接口
有某个角色时候:
没有某个角色的时候:
有某个权力时候:
没有某个权力的时候:
退出系统
原本的token就失效了,我们再访问原本可以访问的接口看看
至此就已经进入尾声了
2020/3/27 新编写了VUE+Element前端页面
正常访问:
非法访问:
重点:当未登录时候访问项目内部页面,由前端控制路由返回登录页,并不会出现可恶的login.jsp,这里我们故意改变数据库token来展示效果。
总结
至于最后没有权利或角色返回的json字符串是因为他抛出AuthorizationException。可以自定义全局异常处理器进行处理。通过这种token达到即可达到前后端分离开发。各位客官,点个赞吧qaq。
2019/11/26日修改:在后续开发中,发现shiro如果使用ShiroConfig中shiroFiltet的map进行权限或角色拦截,会出现只走登陆认证,不走授权认证的情况。这是个巨坑!后续再写一篇文章深究一下。解决方法:使用注解@RequiresRoles() 以及@RequiresPermissions()进行权限和角色拦截
-
-
@Bean("shiroFilter")
-
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
-
ShiroFilterFactoryBean shiroFilter =
new ShiroFilterFactoryBean();
-
shiroFilter.setSecurityManager(securityManager);
-
//自定义过滤(关键)
-
Map<String, Filter> filters =
new HashMap<>();
-
filters.put(
"auth",
new AuthFilter());
-
shiroFilter.setFilters(filters);
-
Map<String, String> filterMap =
new LinkedHashMap<>();
-
//主要是这部分: 不要用这种方法,最好用注解的方法
-
filterMap.put(
"/add",
"roles[admin]");
-
filterMap.put(
"/list",
"roles[admin,user]");
-
filterMap.put(
"/delete",
"perms[admin:delete]");
-
-
filterMap.put(
"/**",
"auth");
-
shiroFilter.setFilterChainDefinitionMap(filterMap);
-
-
return shiroFilter;
-
}
2020/3/25补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为token频繁在客户端和服务器端传输,因此可能会造成token劫持攻击(既黑客捕获你的token之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次token。并且我这里处于方便的原因是采用了mysql存储token,具体开发中应该用redis缓存来存储。
有什么问题可以评论或者私信我,每日在线解(LIAO)疑(SAO)。
我是大誌,一位准备996的卑微码农🐶,觉得好用记得点赞收藏!!!
转载:https://blog.csdn.net/weixin_42236404/article/details/89319359