Shiro整合springboot案例
环境准备
案例介绍
本案例使用shiro+SpringBoot+mybatis进行完成,未使用jsp或者thymeleaf等模板引擎,单纯使用json作为数据传送和返回
主要验证shiro作为权限管理的相关过程
其中包括的相关知识有
- Mybatis中mapper的书写
- springmvc
- Md5加密过程中加盐
salt
和散列使用 - SSM相关过程等
相关依赖
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.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.liulian</groupId>
<artifactId>springboot-shiro-jsp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro-jsp</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<!-- <dependency>-->
<!-- <groupId>org.apache.shiro</groupId>-->
<!-- <artifactId>shiro-spring</artifactId>-->
<!-- <version>1.7.1</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version> 1.7.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-ehcache -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.8</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</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>
目录结构
Shiro相关实现
Shiro配置类
ShiroConfig类
@Configuration
public class ShiroConfig {
// 配置过滤器
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(WebSecurityManager webSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 给filter配置安全管理器
shiroFilterFactoryBean.setSecurityManager(webSecurityManager);
// 配置系统资源
Map<String, String> map = new HashMap<>();
map.put("/user/login", "anon");
map.put("/user/register", "anon");
map.put("/**", "authc");
// 配置默认界面
shiroFilterFactoryBean.setLoginUrl("/user/unlogin");
// 配置过滤规则
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
// 创建安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
webSecurityManager.setRealm(new MyRealm());
return webSecurityManager;
}
// 创建Realm
@Bean
public MyRealm getMyRealm() {
return new MyRealm();
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 若不开启,则所有请求没有授权响应
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
* @param securityManager
* 记住是shiro包下的SecurityManger
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
Realm自定义类
@Component
public class MyRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
User user = userService.findUserByName(principalCollection.getPrimaryPrincipal().toString());
System.out.println("=========》进入授权");
if (!ObjectUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
System.out.println("进行角色授权");
// 添加角色
simpleAuthorizationInfo.addRole(role.getRName());
// 添加权限
List<Perms> perms = role.getPerms();
System.out.println("进行权限授权");
perms.forEach(perm ->{
simpleAuthorizationInfo.addStringPermission(perm.getPName());
});
});
return simpleAuthorizationInfo;
}
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("=============》进入认证");
// 获取用户名
String username = (String) authenticationToken.getPrincipal();
// 从spring容器中获得对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
// 查找是否存在该用户
User user = userService.findUserByName(username);
System.out.println(user);
if (!ObjectUtils.isEmpty(user)) {
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName());
}
return null;
}
}
Spring相关实现
配置文件
application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/springboot-shiro?useSSL=false&useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.liulian.entity
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis.configuration.map-underscore-to-camel-case=true
获取Context工具类
@Configuration
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
控制层 Controller
UserController.Class
/**
* @Author 榴莲男孩
* @Date 2021/7/30 0030 11:20
* @Version 1.0
*/
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@Autowired
UserService userService;
@RequestMapping("/login")
public String login(User user) {
System.out.println("login");
System.out.println(user);
Map map = userService.login(user);
if ((boolean)map.get("boolean")) {
return JSONObject.toJSONString(new ResultVo<User>("200", "认证通过", user));
} else {
return JSONObject.toJSONString(new ResultVo<Object>("200", map.get("msg").toString(), null));
}
}
// 记得开启注解,否则不会回调doGetAuthorizationInfo()方法,无法进行授权
@RolesAllowed({"admin","order"})// 包含的角色可以进行访问
@RequiresPermissions({"order:get:*"})// 包含的权限可以进行访问
@RequestMapping("/index")
public String index() {
System.out.println("login");
return JSONObject.toJSONString(new ResultVo<Object>("200", "首页", null));
}
@RequestMapping("/logout")
public String logout() {
System.out.println("logout");
userService.logout();
return JSONObject.toJSONString(new ResultVo<Object>("200", "注销成功", null));
}
@RequestMapping("/unlogin")
public String unlogin() {
return JSONObject.toJSONString(new ResultVo<Object>("200", "没有登录", null));
}
@PostMapping("/register")
public String registerUser(User user) {
System.out.println("register=============>"+user);
user.setSalt(MyUntil.getSalt(6));
System.out.println(user);
userService.addUser(user);
return ResultVo.successResult();
}
@RolesAllowed({"admin","user"})
@RequiresPermissions({"user:get:*"})
@GetMapping("/all")
public String getAllUser() {
List<User> users = userService.getAllUser();
return ResultVo.successResult(users);
}
}
业务层 Service
UserService接口
public interface UserService {
Map<String,Object> login(User user);
void logout(User user);
void logout();
int addUser(User user);
List<User> getAllUser();
User findUserByName(String username);
User findUserById(String uId);
int deleteById(String uId);
String findSaltByUser(User user);
String findSaltByName(String username);
}
RoleService接口
public interface RoleService {
Role getRoleByRiD(String rId);
Role getRoleByRname(String rName);
Role getRoleByUid(String uId);
List<Role> getAllRole();
}
他们的实现类
UserServiceImp
@Service("userService")
@Transactional // 开启事务
public class UserServiceImp implements UserService {
@Autowired
private UserDao userDao;
@Override
public Map<String,Object> login(User user) {
Subject subject = SecurityUtils.getSubject();
HashMap<String, Object> map = new HashMap<>();
String salt = userDao.findSaltByName(user.getUsername());
String password = user.getPassword();
String passwordHash = MyUntil.getHashHex(password, salt);
// System.out.println("passwordHash========="+passwordHash);
UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), passwordHash);
try {
subject.login(token);
map.put("boolean", true);
} catch (UnknownAccountException e) {
map.put("boolean", false);
map.put("msg", "账号不存在");
} catch (IncorrectCredentialsException e) {
map.put("boolean", false);
map.put("msg", "密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
}finally {
return map;
}
}
@Override
public void logout(User user) {
}
@Override
public void logout() {
Subject subject = SecurityUtils.getSubject();
try {
subject.logout();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public int addUser(User user) {
String passwordHash = MyUntil.getHashHex(user.getPassword(),user.getSalt());
System.out.println(passwordHash);
user.setPassword(passwordHash);
return userDao.addUser(user);
}
@Override
public List<User> getAllUser() {
return userDao.findAllUser();
}
@Override
public User findUserByName(String username) {
return userDao.findUserByName(username);
}
@Override
public User findUserById(String uId) {
return userDao.findUserById(uId);
}
@Override
public int deleteById(String uId) {
return userDao.deleteUserById(uId);
}
@Override
public String findSaltByUser(User user) {
return userDao.findSaltByUser(user);
}
@Override
public String findSaltByName(String username) {
return userDao.findSaltByName(username);
}
}
RoleServiceImp
@Service("roleService")
@Transactional // 开启事务
public class RoleServiceImp implements RoleService {
@Autowired
RoleDao roleDao;
@Override
public Role getRoleByRiD(String rId) {
return roleDao.getRoleByRiD(rId);
}
@Override
public Role getRoleByRname(String rName) {
return roleDao.getRoleByRname(rName);
}
@Override
public Role getRoleByUid(String uId) {
return roleDao.getRoleByUid(uId);
}
@Override
public List<Role> getAllRole() {
return roleDao.getAllRole();
}
}
数据持久层Dao
UserDao接口
@Mapper
@Repository
public interface UserDao {
User findUserByName(String username);
User findUserById(String uId);
String findSaltByUser(User user);
String findSaltByName(String username);
int addUser(User user);
int deleteUserById(String uId);
List<User> findAllUser();
}
RoleDao接口
@Repository
@Mapper
public interface RoleDao {
Role getRoleByRiD(String rId);
Role getRoleByRname(String rName);
Role getRoleByUid(String uId);
List<Role> getAllRole();
}
mapper
UserDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liulian.dao.UserDao">
<resultMap id="userMap" type="user">
<id property="uId" column="u_id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="salt" column="salt"/>
<collection property="roles" ofType="role" javaType="list">
<id property="rId" column="r_id"/>
<result property="rName" column="r_name"/>
<collection property="perms" javaType="list" ofType="perms">
<id property="pId" column="p_id"/>
<result property="pName" column="p_name"/>
</collection>
</collection>
</resultMap>
<resultMap id="roleMap" type="role">
<id property="rId" column="r_id"/>
<result property="rName" column="r_name"/>
<collection property="perms" javaType="list" ofType="perms">
<id property="pId" column="p_id"/>
<result property="pName" column="p_name"/>
</collection>
</resultMap>
<insert id="addUser" parameterType="user">
insert into s_user(username, password, salt)
values (#{username}, #{password}, #{salt})
</insert>
<select id="findAllUser" resultMap="userMap">
select *
from s_user
left join user_role ur on s_user.u_id = ur.u_id
left join s_role on ur.r_id = s_role.r_id
left join role_perm rp on ur.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
</select>
<select id="findUserById" parameterType="String" resultMap="userMap">
select *
from s_user
left join user_role ur on s_user.u_id = ur.u_id
left join s_role on ur.r_id = s_role.r_id
left join role_perm rp on ur.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
where s_user.u_id = #{uId}
</select>
<select id="findUserByName" parameterType="String" resultMap="userMap">
select *
from s_user
left join user_role ur on s_user.u_id = ur.u_id
left join s_role on ur.r_id = s_role.r_id
left join role_perm rp on ur.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
where username = #{username}
</select>
<select id="findSaltByName" resultType="String">
select salt
from s_user
where username = #{username}
</select>
<select id="findSaltByUser" resultType="String">
select salt
from s_user
where u_id = #{uId}
</select>
<delete id="deleteUserById" parameterType="int">
delete
from s_user
where u_id = #{uId}
</delete>
</mapper>
RoleDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liulian.dao.RoleDao">
<resultMap id="roleMap" type="role">
<id property="rId" column="r_id"/>
<result property="rName" column="r_name"/>
<collection property="perms" javaType="list" ofType="perms">
<id property="pId" column="p_id"/>
<result property="pName" column="p_name"/>
</collection>
</resultMap>
<select id="getRoleByRiD" parameterType="String" resultMap="roleMap">
select * from s_role
left join role_perm rp on s_role.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
where s_role.r_id = #{rId}
</select>
<select id="getRoleByRname" parameterType="String" resultMap="roleMap">
select * from s_role
left join role_perm rp on s_role.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
where s_role.r_name = #{rName}
</select>
<select id="getAllRole" resultMap="roleMap">
select * from s_role
left join role_perm rp on s_role.r_id = rp.r_id
left join s_permission sp on rp.p_id = sp.p_id
</select>
</mapper>
实体类
// 用户信息类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String uId;
private String username;
private String password;
private String salt;
private List<Role> roles;
}
// 角色信息类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
private String rId;
private String rName;
private List<Perms> perms;
}
// 权限信息类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Perms {
private String pId;
private String pName;
}
// ResultVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVo<T> {
private String code;
private String msg;
private T date;
public static String successResult() {
return JSONObject.toJSONString(new ResultVo<Object>("200", "成功", null));
}
public static String errorResult() {
return JSONObject.toJSONString(new ResultVo<Object>("200", "失败", null));
}
public static String successResult(Object o) {
return JSONObject.toJSONString(new ResultVo<Object>("200", "成功", o));
}
}
工具类
public class MyUntil {
public static String getSalt(int n) {
char[] chars = "ABCDEFGHIJKKLMNOPQRSTUVWXYZabcdefghijkklmnopqrstuvwxyz1234567890!@#$%^&*()_+~`=".toCharArray();
StringBuffer salt = new StringBuffer();
for (int i = 0; i < n; i++) {
char c = chars[new Random().nextInt(chars.length)];
salt.append(c);
}
return salt.toString();
}
public static String getHashHex(String pwd,String salt,int hash) {
return new Md5Hash(pwd,salt,hash).toHex();
}
public static String getHashHex(String pwd,String salt) {
return new Md5Hash(pwd,salt,2048).toHex();
}
}
结果:
d
权限表:用户名 | 角色 | 具有权限 |
---|---|---|
zhangsan | admin | *😗 |
lisi | user | user:update:*,user:delete:1 |
wangwu | order | order:* |
访问权限表:
url | 角色要求 | 权限要求 |
---|---|---|
/user/index | admin,order | order:get:* |
/user/all | admin,user | user:get:* |
未进行身份验证情况:
// 除登录和注册用户外,所有请求均需要进行身份验证后才能进行访问,若没有进行身份验证,则返回以下信息
{"code":"200","msg":"没有登录"}
登录成功情况
{"code":"200","date":{"password":"123456","username":"lisi"},"msg":"认证通过"}
登录失败情况
{"code":"200","msg":"账号不存在"}
{"code":"200","msg":"密码错误"}
没有权限情况:
{"timestamp":"2021-07-31T09:27:25.588+00:00","status":500,"error":"Internal Server Error","trace":"org.apache.shiro.authz.UnauthorizedException: Subject does not have permission [user:get:*]\r\n\tat
/*
*此处省略打断错误信息
*可以看到,是由于访问了受限资源“/user/all",没有权限Subject does not have permission 而进行报错的
*/
java.lang.Thread.run(Unknown Source)\r\nCaused by: org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public java.lang.String com.liulian.controller.UserController.getAllUser()\r\n\tat org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:90)\r\n\t... 75 more\r\n","message":"Subject does not have permission [user:get:*]","path":"/user/all"}
常见错误集
对访问路径进行了授权但是没有授权成功
在一开始配置的时候,对授权过程进行debug的时候发现并没有进入doGetAuthorizationInfo()
进行授权,从而导致所有请求都直接绕过了shiro
解决方案
在Shiro配置类中添加一下配置
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
在自定义Realm中使用Spring(Service)
- 在进行
身份认证doGetAuthenticationInfo()
和身份授权doGetAuthorizationInfo()
的过程中,需要从数据库中获取相关信息,如数据库中的账号密码、随机盐、用户的角色和权限等等,这时便需要使用Spring容器中的Service组件对数据库进行访问
单纯的@Autowaire
注入并不能获取到当前组件
@Autowired
private UserService userService;
- 需要从Spring应用中获取bean进行调用才行
新建一个工具类,交给spring帮我们管理,我们在有需要的时候调用这个类的静态方法获取bean就可以了
@Configuration
public class ApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
- 在自定义Realm中进行获取bean
// 从spring容器中获得对象
UserService userService = (UserService) ApplicationContextUtils.getBean("userService");
- 这样就能够在自定义Realm中使用数据库啦