shrio框架介绍
shiro是什么呢?
Shiro是一个功能强大,简单易用的 Java 安全框架,提供了用户认证、授权、加密、会话管理、缓存等功能。和目前另一款Java安全框架Spring Security相比,Shiro更加轻便,易于上手。
各功能点介绍
- Authentication:用户身份认证/登录,即验证用户是不是合法用户
- Authorization:用户权限验证,即验证用户是否拥有某个角色,是否拥有某个权限
- Session Manager:会话管理,即用户登录后就存在一个session,在用户注销前,用户的所有信息都会存在与session中
- Crytography:加密,即密码加密存储到数据库
- Web Support:Web支持,即易于集成到Web环境
- Caching:缓存,即用户登录后,其用户信息、角色信息、权限信息会缓存起来,不必每次都去查询数据,提高性能
- Concurrency:支持多线程的并发验证,即在一个线程中开启另一个线程,能把权限自动传播过去
- Testing:提供测试支持
- Run As:允许一个用户假装为另一个用户(如果他们允许的话)的身份进行访问
- Remember Me:记住我,即一次登录后,下次访问就无需登录
shiro流程介绍
Shiro的三大核心组件分别是:Subject
、SecurityManager
和 Realm
。他们直接的关系如下:
- Subject:主体,代表了“当前用户”。这个“当前用户”通常指的是系统中定义的用户对象,它表示的是一个抽象的概念。所有Subject都绑定到Security Manager,与Subject的所有交互都会委托给Security Manager。可以认为Subject是一个门面,而Security Manager才是真正的执行者
- Security Manager:安全管理器,即所有与安全有关的操作都会与Security Manager交互,并且管理者所有Subject,它是Shiro的核心组件。如果了解SpringMVC框架的话,可以将其看成是其中的DispatchServlet
- Realm:域,Shiro从Realm获取数据(用户、角色、权限),就是说Security Manager如果要验证用户的身份,那么他需要从Realm获取相应的用户信息与请求传入的用户信息进行比对,验证是否合法。因此,在Realm相当于开发中的Dao层,其中通常包含了从数据库获取数据的SQL操作。
shiro内部架构介绍
- Subject:主体
- Security Manager:Shiro框架的核心,管理着所有Subject,且负责进行认证、授权、会话以及缓存的管理
- Authenticator:身份认证器,负责身份认证
- Authorizer:授权器,负责控制用户权限验证
- Session Manager:session管理器,负责管理session的生命周期
- Session Dao:Dao是数据库访问对象,负责将session保存到缓存数据库中
- Cache Manager:缓存控制器,管理用户、角色、权限信息的缓存管理
- Cryptography:加密,负责对密码进行加密/解密
- Realms:域,可使用自带的域,或自定义域,获取数据库中的用户、角色、授权信息,为Security Manager提供数据支持
前提准备
本文使用的
springboot版本:2.7.9
mysql版本:8.0.15
mybatis-plus版本:3.1.1
shiro版本:1.4.0
数据库表结构及数据:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_pers
-- ----------------------------
DROP TABLE IF EXISTS `t_pers`;
CREATE TABLE `t_pers` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(80) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_pers
-- ----------------------------
INSERT INTO `t_pers` VALUES (1, 'user:select');
INSERT INTO `t_pers` VALUES (2, 'user:delete');
INSERT INTO `t_pers` VALUES (3, 'user:update');
INSERT INTO `t_pers` VALUES (4, 'user:insert');
INSERT INTO `t_pers` VALUES (5, 'admin:select');
INSERT INTO `t_pers` VALUES (6, 'admin:insert');
INSERT INTO `t_pers` VALUES (7, 'admin:update');
INSERT INTO `t_pers` VALUES (8, 'admin:delete');
-- ----------------------------
-- Table structure for t_role
-- ----------------------------
DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(60) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_role
-- ----------------------------
INSERT INTO `t_role` VALUES (1, 'admin');
INSERT INTO `t_role` VALUES (2, 'user');
-- ----------------------------
-- Table structure for t_role_perms
-- ----------------------------
DROP TABLE IF EXISTS `t_role_perms`;
CREATE TABLE `t_role_perms` (
`id` int NOT NULL AUTO_INCREMENT,
`roleid` int NULL DEFAULT NULL,
`permsid` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_role_perms
-- ----------------------------
INSERT INTO `t_role_perms` VALUES (1, 1, 6);
INSERT INTO `t_role_perms` VALUES (2, 1, 7);
INSERT INTO `t_role_perms` VALUES (3, 1, 8);
INSERT INTO `t_role_perms` VALUES (4, 1, 5);
INSERT INTO `t_role_perms` VALUES (5, 2, 3);
INSERT INTO `t_role_perms` VALUES (6, 2, 4);
INSERT INTO `t_role_perms` VALUES (9, 2, 1);
INSERT INTO `t_role_perms` VALUES (10, 2, 2);
-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(40) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`password` varchar(40) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
`salt` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, 'zhangsan', '9cbcc7faf6f23a57b247610a1fb76b3b', 'sExe#V$X');
INSERT INTO `t_user` VALUES (2, 'lisi', '8f72b3b029adc0541fdbcba30ba25d7e', 'X@i#x^96');
-- ----------------------------
-- Table structure for t_user_role
-- ----------------------------
DROP TABLE IF EXISTS `t_user_role`;
CREATE TABLE `t_user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`userid` int NULL DEFAULT NULL,
`roleid` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb3 COLLATE = utf8mb3_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of t_user_role
-- ----------------------------
INSERT INTO `t_user_role` VALUES (1, 2, 1);
INSERT INTO `t_user_role` VALUES (2, 1, 2);
SET FOREIGN_KEY_CHECKS = 1;
user表用户密码均为:123456
代码已上传至gitee second_dev分支:https://gitee.com/lianaozhe/boot_shiro_demo.git
代码示例
导入依赖
完整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.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.shiro.test</groupId>
<artifactId>boot-shiro-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-shiro-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- hutool 工具包,各种封装功能 一应俱全-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.properties</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.yml</include>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
添加配置
application.yml配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
server:
port: 8080
mybatis-plus:
mapper-locations: classpath*:com/shiro/test/mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# map-underscore-to-camel-case: true
type-aliases-package: com.shiro.test.entity
编写mapper
userMapper类:
package com.shiro.test.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.test.entity.TUser;
import com.shiro.test.entity.vo.UserVo;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author laz
* @since 2023-03-22
*/
public interface TUserMapper extends BaseMapper<TUser> {
/**
* 通过用户名,查询用户角色
* @param username
* @return
*/
List<UserVo> getRoleByUserName(String username);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.shiro.test.mapper.TUserMapper">
<select id="getRoleByUserName" parameterType="String" resultType="com.shiro.test.entity.vo.UserVo">
SELECT
u.id uid,
u.username username,
r.id rid,
r.NAME rname
FROM
t_user u
LEFT JOIN t_user_role ur ON u.id = ur.userid
LEFT JOIN t_role r ON ur.roleid = r.id
WHERE
u.username = #{username}
</select>
</mapper>
userVo:
package com.shiro.test.entity.vo;
import lombok.Data;
/**
* @Author: laz
* @CreateTime: 2023-03-20 15:54
* @Version: 1.0
*/
@Data
public class UserVo {
private Integer uid;
private String username;
private Integer rid;
private String rname;
}
roleMapper类:
package com.shiro.test.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.shiro.test.entity.TRole;
import com.shiro.test.entity.vo.RoleVo;
import java.util.List;
/**
* <p>
* Mapper 接口
* </p>
*
* @author laz
* @since 2023-03-22
*/
public interface TRoleMapper extends BaseMapper<TRole> {
/**
* 通过角色id查询权限信息
* @param id
* @return
*/
List<RoleVo> getPerByRoleId(int id);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.shiro.test.mapper.TRoleMapper">
<select id="getPerByRoleId" resultType="com.shiro.test.entity.vo.RoleVo">
SELECT
r.`id` rid,
r.`name` rname,
p.`id` pid,
p.`name` pname
FROM
t_role r
LEFT JOIN t_role_perms rp ON r.`id` = rp.`roleid`
LEFT JOIN t_pers p ON rp.`permsid` = p.`id`
WHERE
r.`id` = #{id}
</select>
</mapper>
roleVo:
package com.shiro.test.entity.vo;
import lombok.Data;
/**
* @Author: laz
* @CreateTime: 2023-03-20 16:08
* @Version: 1.0
*/
@Data
public class RoleVo {
private Integer rid;
private String rname;
private Integer pid;
private String pname;
}
service层
userservice:
package com.shiro.test.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.shiro.test.entity.TUser;
import com.shiro.test.entity.vo.UserVo;
import com.shiro.test.result.DealResult;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author laz
* @since 2023-03-22
*/
public interface ITUserService extends IService<TUser> {
/**
* 通过用户名查询用户信息
* @param username
* @return
*/
TUser getUserByUserName(String username);
/**
* 登录
* @param tUser
* @return
*/
DealResult login(TUser tUser);
/**
* 根据用户名称查询角色信息
* @param username
* @return
*/
List<UserVo> getRoleByUserName(String username);
}
userservice实现类:
package com.shiro.test.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shiro.test.entity.TUser;
import com.shiro.test.entity.dto.LoginDTO;
import com.shiro.test.entity.vo.UserVo;
import com.shiro.test.exception.LoginException;
import com.shiro.test.mapper.TUserMapper;
import com.shiro.test.result.DealResult;
import com.shiro.test.service.ITUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author laz
* @since 2023-03-22
*/
@Service
public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements ITUserService {
@Override
public TUser getUserByUserName(String username) {
LambdaQueryWrapper<TUser> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TUser::getUsername,username).last("limit 1");
return this.baseMapper.selectOne(wrapper);
}
@Override
public DealResult login(TUser tUser) {
// 获取Subject实例对象,用户实例
Subject user = SecurityUtils.getSubject();
// 将用户名和密码封装到UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken(tUser.getUsername(), tUser.getPassword());
LoginDTO loginDTO = new LoginDTO();
try {
user.login(token);
TUser userInfo = (TUser) user.getPrincipals().getPrimaryPrincipal();
loginDTO.setToken(user.getSession().getId().toString());
BeanUtil.copyProperties(userInfo,loginDTO);
} catch (UnknownAccountException e) {
log.error("账户不存在异常:", e);
throw new LoginException("账号不存在!", e);
} catch (IncorrectCredentialsException e) {
log.error("凭据错误(密码错误)异常:", e);
throw new LoginException("密码不正确!", e);
} catch (AuthenticationException e) {
log.error("身份验证异常:", e);
throw new LoginException("用户验证失败!", e);
}
return DealResult.data(loginDTO);
}
@Override
public List<UserVo> getRoleByUserName(String username) {
return this.baseMapper.getRoleByUserName(username);
}
}
roleservice类:
package com.shiro.test.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.shiro.test.entity.TRole;
import com.shiro.test.entity.vo.RoleVo;
import java.util.List;
/**
* <p>
* 服务类
* </p>
*
* @author laz
* @since 2023-03-22
*/
public interface ITRoleService extends IService<TRole> {
/**
* 通过角色id查询权限信息
* @param id
* @return
*/
List<RoleVo> getPerByRoleId(int id);
}
roleService实现类:
package com.shiro.test.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.shiro.test.entity.TRole;
import com.shiro.test.entity.vo.RoleVo;
import com.shiro.test.mapper.TRoleMapper;
import com.shiro.test.service.ITRoleService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* <p>
* 服务实现类
* </p>
*
* @author laz
* @since 2023-03-22
*/
@Service
public class TRoleServiceImpl extends ServiceImpl<TRoleMapper, TRole> implements ITRoleService {
@Override
public List<RoleVo> getPerByRoleId(int id) {
return this.baseMapper.getPerByRoleId(id);
}
}
shiro配置文件
定义登录异常处理类:
package com.shiro.test.exception;
/**
* @Author: laz
* @CreateTime: 2023-03-22 09:49
* @Version: 1.0
* 异常处理类
*/
public class LoginException extends RuntimeException {
public LoginException() {
super();
}
public LoginException(String message) {
super(message);
}
public LoginException(String message, Throwable cause) {
super(message, cause);
}
}
定义全局异常处理类:
package com.shiro.test.exception;
import com.shiro.test.result.DealResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.management.ServiceNotFoundException;
import java.util.List;
import java.util.Set;
/**
* @Author: laz
* @CreateTime: 2023-03-22 11:49
* @Version: 1.0
* 全局异常处理
*/
@EnableWebMvc
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHand {
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MissingServletRequestParameterException.class)
public DealResult handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
String msg = "缺少请求参数!";
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public DealResult handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
String msg = e.getMessage();
log.error("参数解析失败:", e);
return DealResult.fail(msg);
}
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public DealResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String msg = handleBindingResult(e.getBindingResult());
log.error("方法参数无效: ", e);
return DealResult.fail(msg);
}
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public DealResult handleBindException(BindException e) {
String msg = handleBindingResult(e.getBindingResult());
log.error("参数绑定失败:", e);
return DealResult.fail(msg);
}
/**
* 401 - Unauthorized
*/
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(LoginException.class)
public DealResult handleLoginException(LoginException e) {
String msg = e.getMessage();
log.error("登录异常:", e);
return DealResult.fail(msg);
}
/**
* 403 - Unauthorized
*/
@ResponseStatus(HttpStatus.FORBIDDEN)
@ExceptionHandler(UnauthorizedException.class)
public DealResult handleLoginException(UnauthorizedException e) {
String msg = "用户无权限!";
log.error("用户无权限:", e);
return DealResult.fail(msg);
}
/**
* 405 - Method Not Allowed
*/
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public DealResult handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
String msg = "不支持当前请求方法!";
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 415 - Unsupported Media Type
*/
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public DealResult handleHttpMediaTypeNotSupportedException(Exception e) {
String msg = "不支持当前媒体类型!";
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 422 - UNPROCESSABLE_ENTITY
*/
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
@ExceptionHandler(MaxUploadSizeExceededException.class)
public DealResult handleMaxUploadSizeExceededException(Exception e) {
String msg = "所上传文件大小超过最大限制,上传失败!";
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 500 - Internal Server Error
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(ServiceNotFoundException.class)
public DealResult handleServiceException(ServiceNotFoundException e) {
String msg = "服务内部异常:" + e.getMessage();
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 500 - Internal Server Error
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public DealResult handleException(Exception e) {
String msg = "服务内部异常!" + e.getMessage();
log.error(msg, e);
return DealResult.fail(msg);
}
/**
* 处理参数绑定异常,并拼接出错的参数异常信息。
* <p>
* 创建人:leigq <br>
* 创建时间:2017年10月16日 下午9:09:22 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
*
* @param result
*/
private String handleBindingResult(BindingResult result) {
if (result.hasErrors()) {
final List<FieldError> fieldErrors = result.getFieldErrors();
return fieldErrors.iterator().next().getDefaultMessage();
}
return null;
}
}
定义统一返回结果:
package com.shiro.test.result;
import lombok.Data;
import java.io.Serializable;
/**
* @Author: laz
* @CreateTime: 2022-12-29 11:00
* @Version: 1.0
*/
@Data
public class DealResult implements Serializable {
private static final long serialVersionUID = 1L;
public int code;
public Object data;
public String msg;
public DealResult() {
}
public DealResult(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}
public DealResult(int code, String msg) {
this.code = code;
this.msg = msg;
}
public static DealResult success(String msg){
return new DealResult(200,msg);
}
public static DealResult fail(String msg){
return new DealResult(400,msg);
}
public static DealResult data(Object data){
return new DealResult(200,data,"查询成功!");
}
}
配置自定义 Realm, 用于shiro的认证、授权:
package com.shiro.test.realm;
import com.shiro.test.entity.TRole;
import com.shiro.test.entity.TUser;
import com.shiro.test.entity.vo.RoleVo;
import com.shiro.test.entity.vo.UserVo;
import com.shiro.test.service.ITRoleService;
import com.shiro.test.service.ITUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.util.ObjectUtils;
import java.util.List;
/**
* @Author: laz
* @CreateTime: 2023-03-22 09:31
* @Version: 1.0
* 自定义 Realm, 主要是重写shiro的认证、授权方法
*/
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Autowired
private ITUserService userService;
@Autowired
private ITRoleService roleService;
/**
* 用于授权
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.error("进入授权.......");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
/**
*
* 身份认证的时候new SimpleAuthenticationInfo的第一个参数可以传入对应或者
* username,如果传入user,那么这里就可以获取到user对象的信息,否则只能获取到userName
*/
TUser tUser = (TUser) principalCollection.getPrimaryPrincipal();
//通过用户名查询用户信息
List<UserVo> userRoles = userService.getRoleByUserName(tUser.getUsername());
if (!ObjectUtils.isEmpty(userRoles)){
for (UserVo userVo:userRoles){
authorizationInfo.addRole(userVo.getRname());
//获取角色对应的权限信息
List<RoleVo> perByRoleId = roleService.getPerByRoleId(userVo.getRid());
if (!ObjectUtils.isEmpty(perByRoleId)){
perByRoleId.forEach(per ->{
authorizationInfo.addStringPermission(per.getPname());
});
}
}
}
return authorizationInfo;
}
/**
* 用于认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.error("进入认证.......");
//获取用户账号
String principal = (String) authenticationToken.getPrincipal();
//通过用户名获取到用户信息
TUser tUser = userService.getUserByUserName(principal);
if (ObjectUtils.isEmpty(tUser)){
return null;
}
return new SimpleAuthenticationInfo(tUser,tUser.getPassword(), ByteSource.Util.bytes(tUser.getSalt()),getName());
}
}
添加shiro配置类:
package com.shiro.test.config;
import com.shiro.test.realm.MyRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
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 java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: laz
* @CreateTime: 2023-03-22 09:43
* @Version: 1.0
* shiro配置类
*/
@Configuration
public class ShiroConfig {
/**
*定义shiroFilter工厂,设置对应的过滤条件和跳转条件
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 过滤器链定义映射
Map<String, String> map = new LinkedHashMap<>();
/**
* anon:所有url都可以匿名访问
* authc:所有url都必须认证通过才可以访问
* 过滤链定义,从上向下顺序执行,authc 应放在 anon 下面
*/
//登录接口可以匿名访问
map.put("/user/login","anon");
//所有接口必须认证后才可访问
map.put("/**","authc");
// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了, 位置放在 anon、authc下面
map.put("logout","logout");
//未认证跳转次接口
shiroFilterFactoryBean.setLoginUrl("/user/unAuth");
//未授权跳转次接口
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuthorized");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
/**
* 由于我们的密码校验逻辑是交给Shiro的SimpleAuthenticationInfo进行处理,所以这里需要配置凭证匹配器
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 散列的次数
hashedCredentialsMatcher.setHashIterations(1024);
return hashedCredentialsMatcher;
}
/**
* 将自定义的realm校验方式注入到spring容器中
* @return
*/
@Bean
public MyRealm myRealm() {
MyRealm myRealm = new MyRealm();
myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myRealm;
}
/**
* 权限管理,配置主要是Realm的管理认证
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
/**
* 自定义sessionManager
* @return
*/
@Bean
public SessionManager sessionManager() {
MySessionManager mySessionManager = new MySessionManager();
mySessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
return mySessionManager;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
添加自定义session管理:
package com.shiro.test.config;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* 自定义session管理
* 自定义MySessionManager类继承DefaultWebSessionManager类,重写getSessionId方法
*
* 传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,而在前后端分离的项目中,
* 我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。
*这里采用ajax请求头Authorization携带sessionId的方式
* @author :laz
* @date :2023/3/22 10:21
*/
public class MySessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION = "Authorization";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有 Authorization 则其值为sessionId
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
controller层接口
package com.shiro.test.controller;
import com.shiro.test.entity.TUser;
import com.shiro.test.result.DealResult;
import com.shiro.test.service.ITUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 前端控制器
* </p>
*
* @author laz
* @since 2023-03-22
*/
@Slf4j
@RestController
@RequestMapping("/user")
public class TUserController {
@Autowired
private ITUserService userService;
@PostMapping("/login")
public DealResult login(@RequestBody TUser user){
return userService.login(user);
}
@RequestMapping("/unAuth")
public DealResult unAuth(){
return DealResult.fail("未登录!");
}
@RequestMapping("/unAuthorized")
public DealResult unauthorized(){
return DealResult.fail("没有权限");
}
@PostMapping("insert")
public DealResult insert(@RequestBody TUser user){
return DealResult.success("执行插入方法");
}
}
测试
认证测试
访问UserController的insert方法:
由于我们在ShiroConfig
配置文件中配置了shiroFilterFactoryBean.setLoginUrl(“/user/unAuth”),此行代码含义是**未登录的请求全部重定向到/user/unAuth
这个接口。**这里拦截成功!
先执行/login
接口:
把token放入请求头中再次请求/insert
接口:
这里可以看到,访问成功了。
授权测试
由数据库数据可知,账号zhangsan
拥有user
角色,拥有user:select
,user:insert
,user:update
,user:delete
的权限。
在/insert
接口添加@RequiresPermissions
注解
重启服务器测试:
由于在ShiroConfig
配置了shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuthorized");
,无权限则跳转到/user/unAuthorized
接口,由于zhagnsan
没有admin:insert
权限,所以请求重定向到/user/unAuthorized
。
修改接口权限:
重启服务,再次访问:
可以发现,访问接口成功!
另外授权还有角色相关的注解 @RequiresRoles
,用法和@RequiresPermissions
一样,这里就不演示了。
以上就是shiro认证,授权的全部过程了!