Apache Shiro | Simple. Java. Security.
java语言编写
架构
shiro认证流程
使用
添加shiro依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
SimpleAccountRealm
SimpleAccountRealm只支持role的授权 hasRole、checkRole
授权是认证之后的操作
public void authen() {
//认证的发起者(subject), SecurityManager, Realm
//1. 准备Realm(基于内存存储用户信息)
SimpleAccountRealm realm = new SimpleAccountRealm();
realm.addAccount("admin", "admin", "超级管理员", "商家");
//2. 准备SecurityManager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//3. SecurityManager和Realm建立连接
securityManager.setRealm(realm);
//4. subject和SecurityManager建立联系
SecurityUtils.setSecurityManager(securityManager);
//5. 声明subject
Subject subject = SecurityUtils.getSubject();
//6. 发起认证
subject.login(new UsernamePasswordToken("admin", "admin"));
// 如果认证时,用户名错误,抛出:org.apache.shiro.authc.UnknownAccountException异常
// 如果认证时,密码错误,抛出:org.apache.shiro.authc.IncorrectCredentialsException:
//7. 判断是否认证成功
System.out.println(subject.isAuthenticated());
//8. 退出登录后再判断
// subject.logout();
// System.out.println("logout方法执行后,认证的状态:" + subject.isAuthenticated());
//9. 授权是在认证成功之后的操作!!!
// SimpleAccountRealm只支持角色的授权
System.out.println("是否拥有超级管理员角色:" + subject.hasRole("超级管理员"));
subject.checkRole("商家");
// check方法校验角色时,如果没有指定角色,会抛出异常:org.apache.shiro.authz.UnauthorizedException: Subject does not have role [角色信息]
}
IniRealm
基于文件存储用户名,密码,角色等信息
支持权限校验
public void authen(){
//1. 构建IniRealm
IniRealm realm = new IniRealm("classpath:shiro.ini");
//2. 构建SecurityManager绑定Realm
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(realm);
//3. 基于SecurityUtils绑定SecurityManager并声明subject
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
//4. 认证操作
subject.login(new UsernamePasswordToken("admin","admin"));
//5. 角色校验
// 超级管理员
System.out.println(subject.hasRole("超级管理员"));
subject.checkRole("运营");
//6. 权限校验
System.out.println(subject.isPermitted("user:update"));
// 如果没有响应的权限,就抛出异常:UnauthorizedException: Subject does not have permission [user:select]
subject.checkPermission("user:delete");
}
shiro.ini
[users]
username=password,role1,role2
admin=admin,超级管理员,运营
[roles]
role1=perm1,perm2
超级管理员=user:add,user:update,user:delete
JdbcRealm
通过数据库存储对应的用户、角色、权限信息
推荐使用经典五张表来存储
public void authen(){
//1. 构建JdbcRealm
JdbcRealm realm = new JdbcRealm();
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql:///shiro");
dataSource.setUsername("root");
dataSource.setPassword("root");
realm.setDataSource(dataSource);
// 开启权限校验
realm.setPermissionsLookupEnabled(true);
//2. 构建SecurityManager绑定Realm
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(realm);
//3. 基于SecurityUtils绑定SecurityManager并声明subject
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
//4. 认证操作
subject.login(new UsernamePasswordToken("admin","admin"));
//5. 授权操作(角色)
System.out.println(subject.hasRole("超级管1理员"));
//6. 授权操作(权限)
System.out.println(subject.isPermitted("user:add"));
}
jdbcRealm默认不支持权限校验,需要手动开启setPermissionLookupEnabled(true)
表需要按照它内部的结构来进行定义,需要表结构不一致,也可以使用自定义的校验sql
CustomRealm(自定义)推荐
需要手动创建CustomRealm,并且继承AuthorizingRealm ,
认证
重写doGetAuthenticationInfo方法完成自定义Realm认证
public class CustomRealm extends AuthorizingRealm {
/**
* 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成
* @param token 用户传入的用户名和密码
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1. 基于Token获取用户名
String username = (String) token.getPrincipal();
//2. 判断用户名(非空)
if(StringUtils.isEmpty(username)){
// 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
return null;
}
//3. 如果用户名不为null,基于用户名查询用户信息
User user = this.findUserByUsername(username);
//4. 判断user对象是否为null
if(user == null){
return null;
}
//5. 声明AuthenticationInfo对象,并填充用户信息
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!");
//6. 返回info
return info;
}
校验同其他real基本相同
认证(密码加密加盐)
虽然MD5加密不可逆,但是又一些网站可以把大量常用的密码加密后的结果存储起来,这样MD5的加密也可能会被破解
密码存储的时候需要加密加盐,还需要把对应的salt存储起来,认证时需要拿到对应的salt进行加密然后比较
public class CustomRealm extends AuthorizingRealm {
{
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("MD5");
matcher.setHashIterations(1024);
this.setCredentialsMatcher(matcher);
}
/**
* 认证方法,只需要完成用户名校验即可,密码校验由Shiro内部完成
* @param token 用户传入的用户名和密码
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1. 基于Token获取用户名
String username = (String) token.getPrincipal();
//2. 判断用户名(非空)
if(StringUtils.isEmpty(username)){
// 返回null,会默认抛出一个异常,org.apache.shiro.authc.UnknownAccountException
return null;
}
//3. 如果用户名不为null,基于用户名查询用户信息
User user = this.findUserByUsername(username);
//4. 判断user对象是否为null
if(user == null){
return null;
}
//5. 声明AuthenticationInfo对象,并填充用户信息
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!");
// 设置盐!
info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
//6. 返回info
return info;
}
授权
授权是在认证之后的操作,授权操作需要重写doGetAuthorizationInfo方法
// 授权方法,授权是在认证之后的操作
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//1. 获取认证用户的信息
User user = (User) principals.getPrimaryPrincipal();
//2. 基于用户信息获取当前用户拥有的角色。
Set<String> roleSet = this.findRolesByUser();
//3. 基于用户拥有的角色查询权限信息
Set<String> permSet = this.findPermsByRoleSet(roleSet);
//4. 声明AuthorizationInfo对象作为返回值,传入角色信息和权限信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roleSet);
info.setStringPermissions(permSet);
//5. 返回
return info;
}
shiro整合web的流程
shiro不太适合前后端分离的项目,前后端分离的项目,推荐使用JWT
shiro整合springboot
pom
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
application.yml
shiro:
loginUrl: /login.html
unauthorizedUrl: /401.html # 针对过滤器链生效,针对注解是不生效的
配置类
@Configuration
public class ShiroConfig {
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultRedisWebSessionManager sessionManager = new DefaultRedisWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
// 设置CacheManager,提供与Redis交互的Cache对象
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition shiroFilterChainDefinition = new DefaultShiroFilterChainDefinition();
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/login.html","anon");
filterChainDefinitionMap.put("/user/logout","logout");
filterChainDefinitionMap.put("/user/**","anon");
filterChainDefinitionMap.put("/item/rememberMe","user");
filterChainDefinitionMap.put("/item/authentication","authc");
filterChainDefinitionMap.put("/item/select","rolesOr[超级管理员,运营]");
filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");
filterChainDefinitionMap.put("/**","authc");
shiroFilterChainDefinition.addPathDefinitions(filterChainDefinitionMap);
return shiroFilterChainDefinition;
}
@Value("#{ @environment['shiro.loginUrl'] ?: '/login.jsp' }")
protected String loginUrl;
@Value("#{ @environment['shiro.successUrl'] ?: '/' }")
protected String successUrl;
@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")
protected String unauthorizedUrl;
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
//1. 构建ShiroFilterFactoryBean工厂
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
//2. 设置了大量的路径
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
//3. 设置安全管理器
filterFactoryBean.setSecurityManager(securityManager);
//4. 设置过滤器链
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
//5. 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
filterFactoryBean.getFilters().put("rolesOr",new RolesOrAuthorizationFilter());
//6. 返回工厂
return filterFactoryBean;
}
}
shiro的过滤器
角色校验使用roles
权限校验使用perms
自定义过滤器
写自定义过滤器
public class RolesOrAuthorizationFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
// 获取主体subject
Subject subject = getSubject(request, response);
// 将传入的角色转成数组操作
String[] rolesArray = (String[]) mappedValue;
// 健壮性校验
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
// 开始校验
for (String role : rolesArray) {
if(subject.hasRole(role)){
return true;
}
}
return false;
}
}
将自定义过滤器配置给shiro
shiro配置文件中将自定义过滤器配置进去
@Bean
protected ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition shiroFilterChainDefinition) {
//1. 构建ShiroFilterFactoryBean工厂
ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
//2. 设置了大量的路径
filterFactoryBean.setLoginUrl(loginUrl);
filterFactoryBean.setSuccessUrl(successUrl);
filterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
//3. 设置安全管理器
filterFactoryBean.setSecurityManager(securityManager);
//4. 设置过滤器链
filterFactoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition.getFilterChainMap());
//5. 设置自定义过滤器 , 这里一定要手动的new出来这个自定义过滤器,如果使用Spring管理自定义过滤器,会造成无法获取到Subject
filterFactoryBean.getFilters().put("rolesOr",new RolesOrAuthorizationFilter());
//6. 返回工厂
return filterFactoryBean;
}
加了@Bean注解的方法的参数值也都是从spring容器中获取
springboot项目中,默认有一个过滤器
使用注解授权
@RequiresRoles(value={"role1","role2"})
注解进行授权时,是基于对Controller类进行代理,在前置增强中对请求进行权限校验
在SpringBoot中注解默认就生效,是因为自动装配中,已经配置好了对注解的支持
注解的形式无法将错误页面的信息定位到401.html,因为配置的这种路径,只针对过滤器链有效,注解无效。为了实现友好提示的效果,可以配置异常处理器,@RestControllerAdvice,@ControllerAdvice
记住我,remember me
springboot自动装配
rememberMe是基于user过滤器实现的,适用于安全等级较低的页面
只要登陆过,不需要再次登录
认证登录时,添加rememberMe
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(rememberMe != null && "on".equals(rememberMe));
subject.login(token);
认证后,需要以浏览器的cookie和后台的user对象绑定,进行持久化,所以需要user序列化(实现Serializable)
需要在realm授权方法前重新鉴权(因为cookie绑定的是认证成功后,返回的第一个参数,而第一个参数和授权方法中参数能获得到的用户信息是一个内容。直接在授权方法中先做认证判断 )
Shiro在认证成功后,可以不依赖Web容器的Session,也可以依赖!
在SpringBoot自动装配之后,Shiro默认将HttpSession作为存储用户认证成功信息的位置。
但是SpringBoot也提供了一个基于JVM内存(HashMap)存储用户认证信息的位置。
使用springboot提供的MemorySession来存储用户认证信息:
修改Shiro默认使用的SessionDAO,修改为默认构建好的MemorySessionDAO
// 构建管理SessionDAO的SessionManager
@Bean
public SessionManager sessionManager(SessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
// 将使用MemorySessionDAO的SessionManager注入到SecurityManager
securityManager.setSessionManager(sessionManager);
return securityManager;
}
将认证信息存储在redis中,可以实现分布式系统的功能
重写SessionDAO (extends AbstractSessionDAO),实现redis的相关操作
@Component
public class RedisSessionDAO extends AbstractSessionDAO {
@Resource
private RedisTemplate redisTemplate;
// 存储到Redis时,sessionId作为key,Session作为Value
// sessionId就是一个字符串
// Session可以和sessionId绑定到一起,绑定之后,可以基于Session拿到sessionId
// 需要给Key设置一个统一的前缀,这样才可以方便通过keys命令查看到所有关联的信息
private final String SHIOR_SESSION = "session:";
@Override
protected Serializable doCreate(Session session) {
System.out.println("Redis---doCreate");
//1. 基于Session生成一个sessionId(唯一标识)
Serializable sessionId = generateSessionId(session);
//2. 将Session和sessionId绑定到一起(可以基于Session拿到sessionId)
assignSessionId(session, sessionId);
//3. 将 前缀:sessionId 作为key,session作为value存储
redisTemplate.opsForValue().set(SHIOR_SESSION + sessionId,session,30, TimeUnit.MINUTES);
//4. 返回sessionId
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
//1. 基于sessionId获取Session (与Redis交互)
if (sessionId == null) {
return null;
}
Session session = (Session) redisTemplate.opsForValue().get(SHIOR_SESSION + sessionId);
if (session != null) {
redisTemplate.expire(SHIOR_SESSION + sessionId,30,TimeUnit.MINUTES);
}
return session;
}
@Override
public void update(Session session) throws UnknownSessionException {
System.out.println("Redis---update");
//1. 修改Redis中session
if(session == null){
return ;
}
redisTemplate.opsForValue().set(SHIOR_SESSION + session.getId(),session,30, TimeUnit.MINUTES);
}
@Override
public void delete(Session session) {
// 删除Redis中的Session
if(session == null){
return ;
}
redisTemplate.delete(SHIOR_SESSION + session.getId());
}
@Override
public Collection<Session> getActiveSessions() {
Set keys = redisTemplate.keys(SHIOR_SESSION + "*");
Set<Session> sessionSet = new HashSet<>();
// 尝试修改为管道操作,pipeline(Redis的知识)
for (Object key : keys) {
Session session = (Session) redisTemplate.opsForValue().get(key);
sessionSet.add(session);
}
return sessionSet;
}
}
将RedisSessionDAO交给SessionManager
@Bean
public SessionManager sessionManager(RedisSessionDAO sessionDAO) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
return sessionManager;
}
将SessionManager注入到SecurityManager
@Bean
public DefaultWebSecurityManager securityManager(ShiroRealm realm,SessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
一次请求,访问了多次redis
解决方案:把请求结果放到request中