SpringBoot 集成Shiro(一)
前言
权限管理功能项目中常见的功能之一,SpringBoot关于权限功能的实现技术有Spring Security和Shiro,本文将讲解Spring Boot如何集成Shiro.
Shiro简介
Apache Shiro是一个功能强大且易于使用的Java安全框架,提供了认证,授权,加密,和会话管理。
优点
- 易于使用——易用性是项目的最终目标。应用程序安全非常令人困惑和沮丧,被认为是“不可避免的灾难”。如果你让它简化到新手都可以使用它,它就将不再是一种痛苦了。
- 全面——没有其他安全框架的宽度范围可以同Apache Shiro一样,它可以成为你的“一站式”为您的安全需求提供保障。
- 灵活——Apache Shiro可以在任何应用程序环境中工作。虽然在网络工作、EJB和IoC环境中可能并不需要它。但Shiro的授权也没有任何规范,甚至没有许多依赖关系。
- Web支持——Apache Shiro拥有令人兴奋的web应用程序支持,允许您基于应用程序的url创建灵活的安全策略和网络协议(例如REST),同时还提供一组JSP库控制页面输出。
- 低耦合——Shiro干净的API和设计模式使它容易与许多其他框架和应用程序集成。你会看到Shiro无缝地集成Spring这样的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
- 被广泛支持——Apache Shiro是Apache软件基金会的一部分。项目开发和用户组都有友好的网民愿意帮助。这样的商业公司如果需要Katasoft还提供专业的支持和服务。
核心架构
Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm,三者之间的关系如下图
-
Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
-
SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
-
Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)
数据库设计
权限设计通常采用RBAC即用户、角色、权限、用户-角色、角色-权限5张表。
前期准备
导入jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
业务实现
通过Mybaits-plus 查询用户的信息、用户角色信息、用户的权限信息
业务层
@Service
@Transactional(rollbackFor=Exception.class)
public class UserServiceImpl implements UserService
{
@Autowired
private UserMapper userMapper;
//查询用户信息
@Override
public User getUserByUserId(String userId)
{
Assert.notNull(userId, "userId不能为空");
User user= userMapper.getUserByUserId(userId);
if(user==null)
{
throw new UnknownAccountException("用户名或密码不正确");
}
//
if("0".equals(user.getActive()))
{
throw new UnknownAccountException("用户状态不正确");
}
return user;
}
//获取用户角色
@Override
public List<Role> getRolesByUserOid(Integer userOid)
{
Assert.notNull(userOid, "userOid不能为空");
return userMapper.getRolesByUserOid(userOid);
}
//获取用户权限
@Override
public List<Func> getResByRoleOid(Collection<Integer> roleOids)
{
Assert.notNull(roleOids, "roleOid不能为空");
return userMapper.getResByRoleOid(roleOids);
}
}
Dao层
public interface UserMapper extends BaseMapper<User>
{
User getUserByUserId(String userId);
List<Role> getRolesByUserOid(@Param("userOid")Integer userOid);
List<Func> getResByRoleOid(@Param("roleOids")Collection<Integer> roleOids);
}
配置文件UserMapper.xml
<?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.skywares.fw.security.mapper.UserMapper">
<select id="getUserByUserId" resultType="com.skywares.fw.security.pojo.User">
select
oid,
userId,
userName,
active,
gender,
mobile,
email,
pwd
from fw_security_user u
where u.userId=#{userId}
</select>
<select id="getRolesByUserOid" resultType="com.skywares.fw.security.pojo.Role">
select
r.oid,
r.roleId,
r.active,
r.updateUser,
r.updateDate
from fw_security_user u
join fw_security_user_role ur on ur.userOid = u.oid
join fw_security_role r on r.oid = ur.roleOid
where u.active='1'
and u.oid =#{userOid}
</select>
<select id="getResByRoleOid" resultType="com.skywares.fw.security.pojo.Func">
select
res.oid,
res.resId,
res.defaultLabel,
res.seq,
res.parentoid,
res.url,
res.exturl,
res.type,
res.active
from fw_security_res res
join fw_security_role_res r on r.resOid = res.oid
join fw_security_role role on role.oid = r.roleOid
where role.oid in
<foreach collection="roleOids" item="oid" open="(" close=")" separator=",">
#{oid}
</foreach>
order BY res.oid desc
</select>
</mapper>
Shiro集成
自定义Realm实现认证
public class CustomerRealm extends AuthorizingRealm
{
@Autowired
private UserService userService;
//授权认证
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection)
{
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
User user = (User) principalCollection.getPrimaryPrincipal();
Integer userOid=user.getOid();
List<Role> roleList= userService.getRolesByUserOid(userOid);
//用户角色
Set<String> roleSet=new HashSet<>();
//权限信息
Set<String> funcSet=new HashSet<>();
Set<Integer> roleOids=new HashSet<>();
//查询角色
if(roleList!=null && !roleList.isEmpty())
{
roleList.stream().forEach(t->{
roleSet.add(String.valueOf(t.getRoleId()));
roleOids.add(t.getOid());
});
}
//查询权限
List<Func> funcList= userService.getResByRoleOid(roleOids);
if(funcList!=null && !funcList.isEmpty()){
for(Func func:funcList)
{
funcSet.add(func.getUrl());
}
}
//添加角色
info.addRoles(roleSet);
//添加权限
info.addStringPermissions(funcSet);
return info;
}
//用户认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken authToken) throws AuthenticationException
{
//采用用户名和密码方式
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authToken;
String userId = usernamePasswordToken.getUsername();
//密码
String password = new String(usernamePasswordToken.getPassword());
// 通过用户id获取用户信息
User user = userService.getUserByUserId(userId);
//认证。密码进行加密处理
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPwd(),new CustByteSource(user.getUserId()),getName());
return info;
}
}
说明:doGetAuthenticationInfo:实现用户的认证,本文采用的是用户名和密码的方式。 doGetAuthorizationInfo :加载用户的授权信息 CustByteSource: 用户自定义的加密方式
自定义加密方式
public class CustByteSource implements ByteSource, Serializable
{
private static final long serialVersionUID = -3818806283942882146L;
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
public CustByteSource()
{
}
public CustByteSource(byte[] bytes)
{
this.bytes = bytes;
}
public CustByteSource(char[] chars)
{
this.bytes = CodecSupport.toBytes(chars);
}
public CustByteSource(String string)
{
this.bytes = CodecSupport.toBytes(string);
}
public CustByteSource(ByteSource source)
{
this.bytes = source.getBytes();
}
public CustByteSource(File file)
{
this.bytes = new CustByteSource.BytesHelper().getBytes(file);
}
public CustByteSource(InputStream stream)
{
this.bytes = new CustByteSource.BytesHelper().getBytes(stream);
}
public static boolean isCompatible(Object o)
{
return o instanceof byte[] || o instanceof char[]
|| o instanceof String || o instanceof ByteSource
|| o instanceof File || o instanceof InputStream;
}
@Override
public byte[] getBytes()
{
return this.bytes;
}
@Override
public boolean isEmpty()
{
return this.bytes == null || this.bytes.length == 0;
}
@Override
public String toHex()
{
if (this.cachedHex == null)
{
this.cachedHex = Hex.encodeToString(getBytes());
}
return this.cachedHex;
}
@Override
public String toBase64()
{
if (this.cachedBase64 == null)
{
this.cachedBase64 = Base64.encodeToString(getBytes());
}
return this.cachedBase64;
}
@Override
public String toString()
{
return toBase64();
}
@Override
public int hashCode()
{
if (this.bytes == null || this.bytes.length == 0)
{
return 0;
}
return Arrays.hashCode(this.bytes);
}
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (o instanceof ByteSource)
{
ByteSource bs = (ByteSource) o;
return Arrays.equals(getBytes(), bs.getBytes());
}
return false;
}
private static final class BytesHelper extends CodecSupport
{
/**
* 嵌套类也需要提供无参构造器
*/
private BytesHelper()
{
}
public byte[] getBytes(File file)
{
return toBytes(file);
}
public byte[] getBytes(InputStream stream)
{
return toBytes(stream);
}
}
}
通过shiro提供加密方式针对密码进行加密处理,用户注册获取密码方式如下:
public static final String md5Pwd(String salt,String pwd)
{
//加密方式
String hashAlgorithmName = "MD5";
//盐:为了即使相同的密码不同的盐加密后的结果也不同
ByteSource byteSalt = ByteSource.Util.bytes(salt);
//加密次数
int hashIterations = 2;
SimpleHash result = new SimpleHash(hashAlgorithmName, pwd, byteSalt, hashIterations);
return result.toString();
}
Shiro核心配置
@Configuration
public class ShiroConfig
{
// 自定义密码加密规则
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher()
{
HashedCredentialsMatcher hashedCredentialsMatcher =new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
//true 代表Hex编码,fasle代表采用base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
// 自定义认证
@Bean
public CustomerRealm customerRealm()
{
CustomerRealm customerRealm=new CustomerRealm();
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher());
customerRealm.setCachingEnabled(false);
return customerRealm;
}
//需要定义DefaultWebSecurityManager,否则会报bean冲突
@Bean
public DefaultWebSecurityManager securityManager()
{
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customerRealm());
securityManager.setRememberMeManager(null);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager)
{
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//给filter设置安全管理
factoryBean.setSecurityManager(securityManager);
//配置系统的受限资源
Map<String,String> map = new HashMap<>();
//登录请求无需认证
map.put("/login", "anon");
//其他请求需要认证
map.put("/**", "authc");
//访问需要认证的页面如果未登录会跳转到/login
factoryBean.setLoginUrl("/login");
//访问未授权页面会自动跳转到/unAuth
factoryBean.setUnauthorizedUrl("/unAuth");
factoryBean.setFilterChainDefinitionMap(map);
return factoryBean;
}
/**
* 开启注解方式,页面可以使用注解
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
示例代码
// 登录测试
@Controller
@RequestMapping("")
public class LoginController
{
@RequestMapping("/login")
@ResponseBody
public String login(@RequestParam String userName,@RequestParam String password)
{
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken =new UsernamePasswordToken(userName, password);
subject.login(usernamePasswordToken);
return "成功";
}
/**
* 用户未登录
* @return
*/
@RequestMapping("/unLogin")
public String unLogin()
{
return "login.html";
}
/**
* 用户未授权
* @return
*/
@RequestMapping("/unAuth")
public String unAuth()
{
return "unAuth.html";
}
}
// 角色和权限测试
@RestController
@RequestMapping("/app/sys/user")
public class UserController
{
@RequestMapping("/list")
@RequiresPermissions("/app/sys/user/list")
public String list()
{
return "成功";
}
@RequestMapping("/roleTest")
@RequiresRoles("admin1")
public String roleTest()
{
return "成功";
}
@RequestMapping("/resourceTest")
@RequiresPermissions("/app/sys/user/list1")
public String resourceTest()
{
return "成功";
}
}
说明: @RequiresRoles 用户测试用户失是否包含此角色 @RequiresPermissions :用户是否包含此权限
测试
授权测试
用户未登录,直击访问/app/sys/user/list,则会自动跳转到/login.jsp,可以通过setLoginUrl设置跳转的登录页面
//访问需要认证的页面如果未登录会跳转到/login路由进行登陆
factoryBean.setLoginUrl("/unLogin");
用户登录测试
用户访问/login请求输入正确的用户名和密码
角色测试
@RequestMapping("/roleTest")
@RequiresRoles("admin1")
public String roleTest()
{
return "成功";
}
用户登录成功,访问/roleTest,如果用户包含此角色则返回成功,否则后端会报AuthorizationException异常
权限测试
@RequestMapping("/resourceTest")
@RequiresPermissions("/app/sys/user/list1")
public String resourceTest()
{
return "成功";
}
用户登录成功,访问/resourceTest,如果用户包含此权限则返回成功,否则后端会报AuthorizationException异常
总结
本文讲解SpringBoot集成Shiro框架,实现了基础的身份认证和授权功能,在前后端的分离的环境下大多采用的是token的方式进行登录认证,下一张将讲解Shiro集成JWT实现登录认证功能。
转载自:https://juejin.cn/post/7094907693290225694