文章目录
手把手教你用SpringBoot整合Shiro
1、shiro的前世今生
1.1、shiro是什么?
上面是Apache公司对Shiro的一个简单的介绍。简单总结,Shiro是Apache公司的一个产品,它是一款易于使用的Java安全框架(不仅可以使用在JavaEE上,也可以使用在JavaSE上哦)。它的主要功能有:登录认证、权限认证、加密、会话管理。
1.2、为什么要使用shiro?
我觉得Shiro框架主要是解决Web应用的路径访问的限制问题。举个例子,在一个简单的Web项目,怎么防止未登录或者授权的用户,通过输入路径,来非法访问系统内部的资源呢?当然可以通过自定义filter结合session来实现。但是这方面,Shiro做得明显更好。既然有摩托车,为什么还要使用自行车呢?其次,Shiro还可以对会话进行管理,结合token,还可以解决session在分布式中储存问题,也可以避免伪站请求攻击的问题。所以学习Shiro还是有必要的。
2、Shiro的运作流程
在Shiro当中,主要有以下几个模块:
Subject:当前与程序进行交互的实体(可以是用户,第三方服务,计划任务等)的特定于安全性的“视图”。
Security Manager:SecurityManager是Shiro体系结构的核心。它作为一个环境,用于协调其托管组件以确保它们能够顺利协同工作。它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全性操作。
Authenticator:登录认证器。
Authorizer:授权认证器。
Session Manager:创建和管理用户Session生命周期的组件容器。
Cache Manager:创建和管理Cache组件使用实例的生命周期的组件容器。
Session DAO:可以通过Cache储存Session。
Realm:Realm充当Shiro与应用程序的安全数据之间的“桥梁”或“连接器”。
Cryptography:Shiro的crypto软件中包含易于使用和理解的加密算法,可用于对密码的加密解密。
通过上面的图,我们可以清晰地看到Shiro的执行流程。首先用户通过获取Shiro中的Subject对象,使用Subject对象的login方法进行登录,在构建好的Security Manager环境中,我们通过Realm进行登录的校验和权限的分配。
上面有提到Realm是Shiro与应用程序的安全数据之间的“桥梁”或“连接器”。为什么这样说?是因为我们通过Subject对象,获取登录的账号名,再通过账号名,查询数据库,找到对应的正确密码,然后把账号和密码存放在Realm里。当Subject对象执行login方法登录时,可以进行账号密码的校验。如果正确,则分配具体的权限。如果失败,则返回相应的异常。
那配置Security Manager有什么用呢?全局就只有一个Security Manager,它作为作为Shiro运行的环境,可以绑定许多模块,例如Shiro的拦截器、Realm、SessionManager等。所以一定要配置它。
3、实战–SpringBoot如何整合Shiro
3.1、准备数据库
登录账号表
角色表
权限表
登陆账号、角色关系表
角色、权限关系表
3.2、引入依赖
pom文件引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
3.3、编写底层的CRUD方法
LoginMapper.java
@Repository
public interface LoginMapper {
//根据账号查找用户
@Select("select * from login where username = #{name}")
public Login findone(String name);
//根据编号查找用户
@Select("select * from login where id = #{id}")
public Login findbyid(int id);
//根据账号和密码查找用户
@Select("select * from login where username = #{name} and password = #{pwd}")
public Login findbynameandpwd(String name,String pwd);
}
PermissionMapper.java
@Repository
public interface PermissionMapper {
//使用左外连接,连接角色表和权限表
//根据角色的编号,查找角色对应的权限集合
@Select("SELECT permission.id,permission.`name`,permission.url " +
"FROM role_permission LEFT JOIN permission ON role_permission.p_id=permission.id " +
"WHERE role_permission.r_id= #{roleid}")
public List<Permission> findPermissionListByRoleId(int roleid);
}
RoleMapper.java
@Repository
public interface RoleMapper {
//多表查询,使用左外连接,连接登录表、角色表、权限表
//根据用户名称查找用户对应的角色以及角色对应的权限
@Select("SELECT role.id,role.name,role.description " +
"FROM user_role LEFT JOIN role on user_role.role_id=role.id WHERE user_role.user_id= #{userid}")
@Results(
value = {
@Result(id = true,property = "id",column = "id"),
@Result(property = "name",column = "name"),
@Result(property = "description",column = "description"),
@Result(property = "permissionList",column = "id",
many = @Many(select = "com.example.denglu.dao.PermissionMapper.findPermissionListByRoleId",fetchType = FetchType.DEFAULT))
}
)
public List<Role> findRoleListByid(int userid);
}
3.4、设置CustomRealm
public class CustomRealm extends AuthorizingRealm {
@Autowired
private LoginMapper loginMapper;
@Autowired
private RoleMapper roleMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获得登录账号的账号名称
String name=(String)principalCollection.getPrimaryPrincipal();
//根据账号查找账号对应的角色和权限
Login login=loginMapper.findone(name);
List<Role> rolelist=new ArrayList<>();
List<Permission> permissionList=new ArrayList<>();
List<String> roles=new ArrayList<>();
List<String> permissions=new ArrayList<>();
//通过mybatis接口获取角色和权限
rolelist=roleMapper.findRoleListByid(login.getId());
for(Role role:rolelist){
permissionList=permissionMapper.findPermissionListByRoleId((int)role.getId());
for(Permission permission:permissionList){
permissions.add(permission.getName());
}
roles.add(role.getName());
}
//将登录账号对应的角色和权限返回给customrealm
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(roles);
simpleAuthorizationInfo.addStringPermissions(permissions);
System.out.println("roles"+roles);
System.out.println("permissions"+permissions);
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获得登录账号的账号名称
String username=(String)authenticationToken.getPrincipal();
Login login=loginMapper.findone(username);
String password=login.getPassword();
//判断密码是否为空
if(password==null||"".equals(password)){
return null;
}
//把正确的账号密码返回给customrealm
SimpleAuthenticationInfo simpleAuthenticationInfo=new SimpleAuthenticationInfo(username,password,getName());
return simpleAuthenticationInfo;
}
}
3.5、设置ShiroConfig配置类
@Configuration
public class ShiroConfig {
//设置shiro拦截器
@Bean
public ShiroFilterFactoryBean ShiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
//绑定securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置登录不成功时的返回路径
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
//设置登录成功时的返回路径
shiroFilterFactoryBean.setSuccessUrl("/");
//设置权限不足时的返回路径
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");
//构建一个map集合存放拦截器规则
//必须使用LinkedHashMap,因为这是有序的,过滤规则是从上到下匹配。
Map<String,String> filterChainDefintionMap=new LinkedHashMap<>();
//用户退出
filterChainDefintionMap.put("/logout","logout");
//游客模式,表示这里的请求任何人都能访问
filterChainDefintionMap.put("/pub/**","anon");
//需要先登录,只有登录了才有资格访问这里的请求
filterChainDefintionMap.put("/authc/**","authc");
//需要符合角色,只有拥有这个角色身份的人才可以访问下列路径
filterChainDefintionMap.put("/admin/**","roles[admin]");
//需要符合权限,只有拥有这个权限的人可以访问下列路径
filterChainDefintionMap.put("/video/update","perms[video_update]");
//防止有路径遗漏
filterChainDefintionMap.put("/**","authc");
//绑定过滤器规则的map
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefintionMap);
return shiroFilterFactoryBean;
}
//设置shiro的securitymanager环境
@Bean
public SecurityManager securityManager(){
DefaultSecurityManager defaultSecurityManager=new DefaultWebSecurityManager();
//绑定sessionmanager
defaultSecurityManager.setSessionManager(sessionManager());
//绑定realm
defaultSecurityManager.setRealm(customRealm());
return defaultSecurityManager;
}
//设置realm
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm=new CustomRealm();
//绑定密码使用的加密方式
//customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
//设置sessionmanager
@Bean
public SessionManager sessionManager(){
CustomSessionManager customSessionManager=new CustomSessionManager();
//设置token过期时间为20s
customSessionManager.setGlobalSessionTimeout(20000);
return customSessionManager;
}
//设置密码使用的加密方式
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();
//设置使用的加密方式是md5
credentialsMatcher.setHashAlgorithmName("md5");
//加密2次
credentialsMatcher.setHashIterations(2);
return credentialsMatcher;
}
}
3.6、设置CustomSessionManager
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String AUTHORIZATION="token";
public CustomSessionManager(){
super();
}
//重写获得sessionid的方法
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
//前端请求头必须传入token值
String sessionid=WebUtils.toHttp(request).getHeader(AUTHORIZATION);
//如果请求头中有token值,则把token值设置为sessionid
if(!StringUtils.isEmpty(sessionid)){
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,sessionid);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE);
return sessionid;
}else {
//如果没有,则按照原来的从cookie中读取sessionid
return super.getSessionId(request,response);
}
}
}
3.7、编写对应的Controller
PublicController.java
@RestController
@RequestMapping("/pub")
public class PublicController {
@RequestMapping("/need_login")
public JsonData needlogin(){
return JsonData.buildSucess(-2,"请先登录");
}
@RequestMapping("/not_permit")
public JsonData notpermit(){
return JsonData.buildSucess(-3,"没有权限");
}
@RequestMapping("/index")
public JsonData index(){
List<String> videolist=new ArrayList<>();
videolist.add("假面骑士");
videolist.add("奥特曼");
return JsonData.buildSucess(videolist);
}
@PostMapping("/login")
public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response){
//获取subject操作主体
Subject subject= SecurityUtils.getSubject();
Map<String,Object> map=new HashMap<>();
//获取登录输入的账号密码
UsernamePasswordToken token=new UsernamePasswordToken(userQuery.getUsername(),userQuery.getPassword());
try{
//使用subject主体的login方法登录
subject.login(token);
map.put("msg","登录成功");
map.put("session_id",request.getSession().getId());
return JsonData.buildSucess(map);
}catch (UnknownAccountException e){
map.put("msg","用户名不存在");
return JsonData.buildError(map);
}catch (IncorrectCredentialsException e){
map.put("msg","密码错误");
return JsonData.buildError(map);
}
}
}
AdminController.java
@RestController
@RequestMapping("/admin")
public class AdminController {
@RequestMapping("/findorder")
public JsonData findorder(){
Map<String,Object> map=new HashMap<>();
map.put("购买红米手机",1000);
map.put("购买苹果手机",4000);
return JsonData.buildSucess(map);
}
}
OrderController.java
@RestController
@RequestMapping("/authc")
public class OrderController {
@RequestMapping("/video/playrecord")
public JsonData playrecord(){
Map<String,Object> map=new HashMap<>();
map.put("火影忍者","第一集");
map.put("海贼王","第四十八集");
return JsonData.buildSucess(map);
}
}
VideoController.java
@RestController
@RequestMapping("/video")
public class VideoController {
@RequestMapping("/update")
public JsonData update(){
return JsonData.buildSucess("更新成功");
}
}
3.8、测试
下面我们使用Postman对接口进行测试。
用户直接登录,获取token值
请求头不添加token值,直接访问需要登录的接口,访问失败
请求头添加token值,直接访问需要登录的接口,访问成功
当前登录的账号没有访问此接口的权限
使用拥有权限的账号登录后,重新访问此接口