需要一个整合了mybatis的springboot项目;
这部分可以参考我之前的文章。
使用idea创建springboot项目并整合mybatis
shiro是一个开源的权限管理框架,其功能非常丰富,这里我只实现它最基本的登录认证和权限验证功能,我使用的开发环境是:
idea 2018.1
springboot 2.1.12
shiro-spring 1.3.2
mysql 8.0.19
jdk 1.8
首先在pom.xml中加入shiro-spring依赖
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.3.2version>
dependency>
使用shiro之前,需要对shiro进行一些自定义配置,
package com.lg.shirodemo.common;
import com.lg.shirodemo.filter.ShiroLoginFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
private final static Logger log = LoggerFactory.getLogger(ShiroConfig.class);
/**
* 创建ShrioFilterFactoryBean,配置filter规则
* @Shiro内置过滤器
* anon org.apache.shiro.web.filter.authc.AnonymousFilter
* authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
* authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
* perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
* port org.apache.shiro.web.filter.authz.PortFilter
* rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
* roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
* ssl org.apache.shiro.web.filter.authz.SslFilter
* user org.apache.shiro.web.filter.authc.UserFilter
* @return
*/
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 添加自定义filters
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
//添加未通过认证时不跳转页面,而是返回json
filters.put("authc", new ShiroLoginFilter());
/*
* 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
* Shiro内置过滤器,可以实现权限相关的拦截器,常用的有:
* anon:无需认证(登录)即可访问
* authc:必须认证才可以访问
* user:如果使用rememberme的功能可以直接访问
* perms:该资源必须得到资源权限才能访问
* role:该资源必须得到角色资源才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
//放过登录请求
filterMap.put("/login", "anon");
filterMap.put("/addUser", "anon");
filterMap.put("/**", "authc");//其他资源全部拦截
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
//如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
// 这里可以配置默认登录地址
//shiroFilterFactoryBean.setLoginUrl("/unauth");
return shiroFilterFactoryBean;
}
/**
* 创建DefaultWebSecurityManager
* SecurityManager 是 Shiro 架构的核心,通过它来链接Realm和用户(文档中称之为Subject.)
* @return
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//注入自定义cookie管理
//securityManager.setRememberMeManager(cookieRememberMeManager());
//注入自定义session管理
//securityManager.setSessionManager(sessionManager());
//注入自定义cache管理
//securityManager.setCacheManager(redisCacheManager());
//注入自定义realm
securityManager.setRealm(userRealm());
return securityManager;
}
/**
* 创建Realm
* @return
*/
@Bean(name = "userRealm")
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
//设置解密规则
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
//不使用默认的缓存
//userRealm.setCachingEnabled(false);
return userRealm;
}
/**
* 设置shiro验证密码时的解密方式
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//使用MD5散列算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列1次
hashedCredentialsMatcher.setHashIterations(1);
//storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
}
这个配置文件里可以对shiro进行多种自定义配置,比如实现自己的session管理,实现redis缓存,实现cookie管理等等,这些可以查看官方文档进行设置。
在设置过滤器的时候,我自己定义了一个无权限过滤器,是为了防止shiro在登录失败的时候默认跳转/login.jsp,实现前后端分离,直接返回json数据
package com.lg.shirodemo.filter;
import com.lg.shirodemo.util.JsonUtil;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* 重写authc过滤器
* shiro框架默认跳转到/login.jsp
* 前后端分离模式不需要跳转,返回json数据就行
* @author lg
*
*/
public class ShiroLoginFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
httpServletResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
Map<String, Object> resultData = new HashMap<>();
resultData.put("msg", "登录认证失败,请重新登录!");
resultData.put("status", 403);
httpServletResponse.getWriter().write(JsonUtil.toJson(resultData));
return false;
}
}
package com.lg.shirodemo.common;
import com.lg.shirodemo.dto.LoginInfo;
import com.lg.shirodemo.mapper.UserExtendMapper;
import com.lg.shirodemo.table.entity.Menu;
import com.lg.shirodemo.table.entity.Role;
import com.lg.shirodemo.table.entity.User;
import com.lg.shirodemo.table.entity.UserExample;
import com.lg.shirodemo.table.mapper.UserMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 自定义shiro的realm,用来认证和鉴权
*/
public class UserRealm extends AuthorizingRealm {
private final static Logger log = LoggerFactory.getLogger(UserRealm.class);
@Autowired
private UserMapper userMapper;
@Autowired
private UserExtendMapper userExtendMapper;
/**
* 认证(登陆时候调用)
* @author lg
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("自定义认证(登陆时候调用)开始---------");
UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken;
String loginName = token.getUsername();
// 从数据库获取该用户信息
UserExample ue = new UserExample();
ue.createCriteria().andLoginNameEqualTo(loginName);
List<User> users = userMapper.selectByExample(ue);
if (null == users || users.size() < 0){
//没有返回登录用户名对应的SimpleAuthenticationInfo对象时,就会在LoginController中抛出UnknownAccountException异常
return null;
}
User user = users.get(0);
LoginInfo loginInfo = new LoginInfo();
loginInfo.setUser(user);
List<Role> roles = userExtendMapper.getRolesByUserId(user.getId());
loginInfo.setRoleList(roles);
if(roles != null && roles.size() > 0){
Role role = roles.get(0);
List<Menu> menus = userExtendMapper.getMenusByRoleId(role.getId());
loginInfo.setMenuList(menus);
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
//这里的第一个参数,可以是查询到的用户实体,也可以是用户名。主要是为方便后期Subject的getPrincipal()方法取值。放进去是什么,getPrincipal()取到的就是什么。
loginInfo,
//这里的密码,一定是查询到的实体密码,不是参数传递的密码
user.getPassword(),
//加盐salt=userName+salt
ByteSource.Util.bytes(user.getLoginName()+"salt"),
//?
getName()
);
return authenticationInfo;
}
/**
* 授权(验证权限时候调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) throws UnauthorizedException {
log.info("自定义授权(验证权限时候调用)开始------------");
LoginInfo loginInfo = (LoginInfo) principalCollection.getPrimaryPrincipal();
Set<String> roleSet = new HashSet<>();
Set<String> permissionSet = new HashSet<>();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roles = loginInfo.getRoleList();
if (roles != null && roles.size() > 0) {
// 为了简单,一个用户设计只有一个角色
Role role = roles.get(0);
roleSet.add(role.getName());
simpleAuthorizationInfo.addRoles(roleSet);
List<Menu> menus = loginInfo.getMenuList();
//将该角色拥有的权限添加到shiro鉴权器中
for (Menu menu : menus) {
permissionSet.add(menu.getName());
simpleAuthorizationInfo.addStringPermissions(permissionSet);
}
}
return simpleAuthorizationInfo;
}
}
现在数据库中创建经典的用户-角色-权限模型
CREATE TABLE `sys_user` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`login_name` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`user_name` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`password` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`tel` varchar(16) COLLATE utf8mb4_general_ci DEFAULT NULL,
`enable` varchar(1) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `sys_role` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `sys_user_role` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`user_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`role_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `sys_menu` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(128) COLLATE utf8mb4_general_ci DEFAULT NULL,
`parent_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`url` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`sort_no` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `sys_role_menu` (
`id` varchar(32) COLLATE utf8mb4_general_ci NOT NULL,
`role_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
`menu_id` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
创建表后,处理好mybatis的查询(此处不详细说明了),创建登录的controller
@RequestMapping("/login")
public Map<String, Object> login(User user){
Map<String, Object> res = new HashMap<>();
// 登录失败从request中获取shiro处理的异常信息。
UsernamePasswordToken token = new UsernamePasswordToken(user.getLoginName(), user.getPassword());
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
//从UserRealm里返回的SimpleAuthenticationInfo获取到认证成功的用户名,
//subject.getPrincipal()获取的是SimpleAuthenticationInfo设置的第一个参数
LoginInfo loginInfo = (LoginInfo) subject.getPrincipal();
Session session = subject.getSession();
session.setAttribute("loginUser", loginInfo);
loginInfo.getUser().setPassword(null);
res.put("data", loginInfo);
res.put("token", subject.getSession().getId());
res.put("status", 0);
}catch (IncorrectCredentialsException e){
res.put("status", -1);
res.put("msg", "密码错误");
}catch (LockedAccountException e){
res.put("status", -1);
res.put("msg", "账号被冻结");
}catch (AuthenticationException e) {
res.put("status", -1);
res.put("msg", "账号不存在");
}catch (Exception e) {
log.error(e.getMessage());
}
return res;
}
因为/login这个地址在之前的shiroConfig配置中设为了不拦截,所以可以直接访问,但是访问其他路径时
而登录/login成功后,返回用户的基本信息。
那么登录验证实现了,如何实现鉴权呢?方案之一就是使用俩个注解添加在controller方法上:
注意!!!这些注解是基于aop实现的,所以必须要确认你的springboot是否添加了aop依赖
org.springframework.boot
spring-boot-starter-aop
@RequiresRoles("admin") //需要用户是拥有admin角色
@RequiresPermissions("updateUser") //需要用户有updateUser权限
测试结果,如果没有该角色或该权限,会抛出异常,这里可以对所有controller做一个全局异常处理。
至此,springboot整合shiro权限管理基本功能已经实现完毕。