Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单,对比 Spring Security,可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,它可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等工作,所以使用小而简单的 Shiro 就足够了。
本文内容不会叙述概念和理论本文力求简单清晰,目标读者是shiro的入门者,为具有一定理论基础的同学提供一个实操的demo希望有助于shiro的入门学习。
(1)实体设计:Shiro的权限体系包括用户、角色和权限三个概念,因此在本demo中将其对应设计为用户类、角色类和权限类,三者关逻辑关系如图所示:
(2)功能设计:本demo仅实现,用户登录、权限判断。
使用springboot搭建一个简单工程,无需复杂只要能支持shiro权限验证即可。
4.0.0
microapps.com.cn
shiro
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-parent
2.2.0.RELEASE
org.springframework.boot
spring-boot-starter
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-thymeleaf
org.apache.shiro
shiro-spring
1.4.0
org.springframework.boot
spring-boot-starter-thymeleaf
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-maven-plugin
true
spring:
server:
port: 8080
thymeleaf:
# 页面编码
encoding: UTF-8
# 页面模式
mode: HTML
# 禁止缓存生成环境须开放缓存
cache: false
servlet:
content-type: text/html
在springboot工程中集成shiro很简单,Shiro有三个核心组件:Subject, SecurityManager 和 Realms,只需将这个三个组件集成进工程即。
Realms:获取安全数据(如用户、角色、权限),在我们工程中需要使用者自定义该类,并实现身份认证和权限认证两个方法。
package microapps.com.cn.shirodemo.shiro;
import microapps.com.cn.shirodemo.domain.Permissions;
import microapps.com.cn.shirodemo.domain.Role;
import microapps.com.cn.shirodemo.domain.User;
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.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* @Description: 权限信息的验证类
* @Author: liuhe
* @Date: 2020/10/10
*/
public class CustomRealm extends AuthorizingRealm {
/**
* 授权,即角色或者权限验证
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = this.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getName());
//添加权限
for (Permissions permissions : role.getPerms()) {
simpleAuthorizationInfo.addStringPermission(permissions.getName());
}
}
return simpleAuthorizationInfo;
}
/**
* 身份认证/登录
*
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户名
String name = authenticationToken.getPrincipal().toString();
//获取用户密码
String password = new String((char[]) authenticationToken.getCredentials());
User user = this.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
}
if (!user.getPassword().equals(password)) {
return null;
}
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo
= new SimpleAuthenticationInfo(name, user.getPassword(), getName());
return simpleAuthenticationInfo;
}
/**
* 此方法模拟在数据库中查询用户信息
* @param username 用户名
* @return 用户对象
*/
private User getUserByName(String username) {
User user = null;
if("admin".equals(username)) {
Permissions p1 = new Permissions("1", "user:query");
Permissions p2 = new Permissions("2", "user:add");
Permissions p3 = new Permissions("3", "user:del");
Permissions p4 = new Permissions("3", "user:edit");
List permissionsSet = new ArrayList<>();
permissionsSet.add(p1);
permissionsSet.add(p2);
permissionsSet.add(p3);
permissionsSet.add(p4);
Role role = new Role("1", "admin", permissionsSet);
List roleSet = new ArrayList<>();
roleSet.add(role);
user = new User("1", "zhangsan","123456", roleSet);
}
return user;
}
}
SecurityManager:它所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互。通过ShiroConfig类配置SecurityManager。
package microapps.com.cn.shirodemo.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import microapps.com.cn.shirodemo.shiro.CustomRealm;
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.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.mgt.SecurityManager;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Description: Shiro配置类
* @Author: liuhe
* @Date: 2020/10/10
*/
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//将自己的验证方式加入容器
@Bean
public CustomRealm myShiroRealm() {
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
/**
* 权限管理,配置主要是Realm的管理认证
* @return
*/
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login"); //登录
shiroFilterFactoryBean.setSuccessUrl("/index"); //首页
LinkedHashMap filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不需要权限过滤的路径
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/logout","logout");
// 配置需要权限过滤的路径
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 配置thymeleaf支持shiro标签
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
}
Subject:它代表了当前 “用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等,在应用中Subject主要是嵌入到我们的业务代码中,例如在下面登录Controller中就用来执行登录。
package microapps.com.cn.shirodemo.controller;
import lombok.extern.slf4j.Slf4j;
import microapps.com.cn.shirodemo.domain.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
/**
* @Description: 登录控制器类
* @Author: liuhe
* @Date: 2020/10/10
*/
@Slf4j
@Controller
public class LoginController {
/**
* 跳转登录页
* @param model
* @return
*/
@GetMapping("/login")
String login(Model model) {
return "login";
}
/**
* 提交登录信息
* @param user 登录用户对象
* @return 验证通过转向index页
*/
@PostMapping("/login")
public String login(User user) {
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken
= new UsernamePasswordToken(
user.getUsername(),
user.getPassword()
);
try {
//进行验证,这里可以捕获异常,然后返回对应信息
subject.login(usernamePasswordToken);
}catch (AuthorizationException e) {
log.error("没有权限!", e);
return "403";
}
return "redirect:/index";
}
/**
* 首页
* @param model model对象
* @return
*/
@GetMapping("/index")
String index(Model model) {
return "index";
}
}
另外,Shiro的错误跳转我是使用全局异常捕获来进行处理的。
package microapps.com.cn.shirodemo.exception;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @Description: 全局异常处理类
* @Author: liuhe
* @Date: 2020/10/14
*/
@ControllerAdvice
@Slf4j
public class ShiroExceptionHandler {
@ExceptionHandler
public String unknownAccountExceptionHandler(UnknownAccountException e) {
log.error("用户名或密码错误!", e.getMessage());
return "400";
}
@ExceptionHandler
public String unauthorizedExceptionHandler(UnauthorizedException e) {
log.error("没有通过权限验证!", e.getMessage());
return "403";
}
}
定义受管控资源控制器类
package microapps.com.cn.shirodemo.controller;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description: 用户控制器类
* @Author: liuhe
* @Date: 2020/10/10
*/
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 测试方法:
* 要求具有admin用户角色并同时
* 具备user:edit权限才能访问
* @return
*/
@GetMapping("/admin")
@RequiresRoles("admin")
@RequiresPermissions("user:edit")
public String admin(Model model) {
model.addAttribute("msg","admin success");
return "200";
}
/**
* 测试方法:
* 要求具有customer用户角色并同时
* 具备user:query权限才能访问
* @return
*/
@GetMapping("/customer")
@RequiresRoles("customer")
@RequiresPermissions("user:query")
public String customer(Model model){
model.addAttribute("msg","customer success");
return "200";
}
}
login
Hello, 注销
1.启动工程使用admin用户登录
2.进入index页访问受控资源
3.进入有权限的admin功能项
4.进入没有权限的customer功能项
以上就是本demo的全部功能,在下一篇博客我将在此基础上使用Redis做缓存和共享Session。