SpringBoot+SpringJPA+Swagger+Shiro 快速搭建前后端分离的权限管理系统
前前后端分离,一般都是通过 token\实现,本项目也是一样;用户登录时,生成 token 及 token 过期时间,token 与用户是一一对应关系,调用接口的时候,把\token\放到\header 或 请求参数\中,服务端就知道是谁在调用接口。
Let's do it!!
介绍:这次我们使用 Shiro 快速搭建前后端分离的权限管理系统 利用 JPA 帮我们管理数据库,Swagger Knife4j 帮我搭建 Web 测试环境;
后台基于 SpringBoot JPA Knife4j Shiro
前端基于 VUE ElementUI
注意:主要观察 token 的使用方法!
第一步:新建工程
pom 文件 application.yml 巴拉巴拉这里省略,这里贴出需要用到的依赖:
html
<!--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>
第二步:准备好要用的包包和类类
第三步:编写登陆入口
为了方便这里不做密码加盐加密:
```java @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 返回给前端。
```java /* * @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 类
```java /* * @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 shiroFilter(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 接口,就可以了。
java
//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 中却可以,可能是版本的问题)
为了避免误导,将上诉代码 AuthenticationToken 修改为 UsernamePasswordToken,并且走了一下源码,发现这个 getAuthenticationTokenClass()实际上获取到的是 UsernamePasswordToken.class
再回头看看 renren-fast 中的源码,原来他重写了 supports 方法!
第七步:编写自己的 Realm
发起请求时,接受传过来的 token 后,如何保证 token 有效及用户权限呢?调用接口时,接受传过来的 token 后,如何保证 token 有效及用户权限呢?其实,Shiro 提供了 AuthorizingRealm 以及 AuthenticatingFilter 抽象类,继承 AuthorizingRealm 和 AuthenticatingFilter 抽象类重写方法即可。
```java /* * @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。
```java /* * 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;
}
} ```
第九步:详解校验流程
先给你们上一个超级详细的流程图。
接着我们打上断点按照代码走走,可能会有点啰嗦。
- 前端发起请求首先会进入 AuthFilter 的 isAccessAllowed(),除了 OPTION 方法,其余都拦截。
- 拦截之后进入 AuthFilter 的 onAccessDenied(),这里获取 token 后判断 token 是否 isBlank。如果是,代表请求未携带 token,直接默认返回 400,未登录给前端,流程就结束了。如果携带了 token 则进入第三步,继续流程。
- 接着进入 AuthFilter 的 createToken,这里生成我们自定义的 AuthToken 对象。
- 接着就会来到 AuthRealm 中的 doGetAuthenticationInfo(),在这个方法中继续 token 的有效性校验,例如过期、和数据库的 token 对不上(用户已退出)的情况。如果校验失败,进入第 5 步,否则进入第 6 步。
- token 失效后回到 AuthFilter 中的 onLoginFailure(),返回 400 以及 msg,流程结束。
- Token 校验成功后进入 AuthRealm 的 doGetAuthorizationInfo(),进行获取当前用户拥有的权限,之后底层代码会进行权限验证。如果用户有权限则会进入请求方法,否则抛出异常。到这一步校验过程就结束了。
看看效果
终于熬完上面的步骤了,这时候总体的架构已经确立好了,下面让我们来看看效果如何
DTO
java
/**
* 登录传输类
*/
@Data
public class LoginDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@NotBlank(message = "密码不能为空")
private String password;
}
实体类
```java @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 |
```sql / 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。
```java /* * @Author 大誌 * @Date 2019/4/7 15:20 * @Version 1.0 / @RestController 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 异常处理器,用于捕获无权限时候的异常。
```java @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()进行权限和角色拦截
```java
@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;
}
```
补充,修改了部分不符合规范的代码,添加了全局异常捕获器。同时补充了校验流程。同时提示两句,因为 token 频繁在客户端和服务器端传输,因此可能会造成 token 劫持攻击(既黑客捕获你的 token 之后就可以代替你为所欲为),如果对这方面有安全隐患的担忧,可以采取每访问一次接口,更新一次 token。并且我这里处于方便的原因是采用了 MySQL 存储 token,具体开发中应该用 Redis 缓存来存储。
参考文献
- 通用权限管理系统研究与应用(电子科技大学·杨莹莹)
- 基于OAuth2.0协议的企业分布式授权系统设计与实现(华中科技大学·支猛)
- 统一认证授权平台的构建(成都理工大学·李勇)
- 基于SSH资源管理系统的设计及实现(西安电子科技大学·杨静涛)
- 基于Java EE平台的个人健康信息管理系统设计与实现(南京邮电大学·贾顺贺)
- 基于EPP的域名管理系统(山东大学·孟庆领)
- 基于Dubbo框架的开发者中心的设计与实现(北京交通大学·涂鹏程)
- 基于Java EE平台的个人健康信息管理系统设计与实现(南京邮电大学·贾顺贺)
- 统一认证授权平台的构建(成都理工大学·李勇)
- 基于Struts的通用权限管理系统的设计与实现(电子科技大学·万静)
- 基于SSH资源管理系统的设计及实现(西安电子科技大学·杨静涛)
- 统一认证授权平台的构建(成都理工大学·李勇)
- 项目管理原型系统—权限管理子系统的设计与实现(电子科技大学·甘沅鑫)
- 基于OAuth2.0协议的企业分布式授权系统设计与实现(华中科技大学·支猛)
- 办公流程管理系统的设计与实现(北京邮电大学·王影)
本文内容包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主题。发布者:源码导航 ,原文地址:https://bishedaima.com/yuanma/35844.html