一、Shiro简介
二、项目实现
2.1 数据库结构
2.2 SQL
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`menu_id` int(32) NOT NULL AUTO_INCREMENT,
`menu_ name` varchar(200) NOT NULL,
`parent_id` int(32) NOT NULL DEFAULT '0',
`url` varchar(250) NOT NULL DEFAULT '#',
`menu_type` int(3) NOT NULL,
`perms` varchar(250) NOT NULL,
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('1', '用户管理', '0', '#', '1', '');
INSERT INTO `menu` VALUES ('2', '添加用户', '1', '/user/add', '2', 'user:add');
INSERT INTO `menu` VALUES ('3', '分页查询用户', '1', '/user/page', '2', 'user:page');
INSERT INTO `menu` VALUES ('4', '修改用户', '1', '/user/update', '2', 'user:update');
INSERT INTO `menu` VALUES ('5', '删除用户', '1', '/user/delete', '2', 'user:delete');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` int(32) NOT NULL AUTO_INCREMENT,
`role_name` varchar(200) DEFAULT NULL,
`remark` varchar(255) DEFAULT NULL,
`delete_flag` int(3) DEFAULT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('1', 'admin', '系统管理员', '2');
INSERT INTO `role` VALUES ('2', 'biz', '业务用户', '2');
-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`role_menu_id` int(32) NOT NULL AUTO_INCREMENT,
`role_id` int(32) NOT NULL,
`menu_id` int(32) NOT NULL,
PRIMARY KEY (`role_menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES ('1', '1', '1');
INSERT INTO `role_menu` VALUES ('2', '1', '2');
INSERT INTO `role_menu` VALUES ('3', '1', '3');
INSERT INTO `role_menu` VALUES ('4', '1', '4');
INSERT INTO `role_menu` VALUES ('5', '1', '5');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` bigint(32) NOT NULL AUTO_INCREMENT,
`user_name` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
`salt` varchar(255) NOT NULL,
`del_flag` int(3) NOT NULL DEFAULT '2',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', 'admin', '14e1b600b1fd579f47433b88e8d85291', '14e1b600b1fd579', '2');
INSERT INTO `user` VALUES ('2', 'biz', '14e1b600b1fd579f47433b88e8d85291', '579f47433b88e8d85291', '2');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_role_id` int(32) NOT NULL AUTO_INCREMENT,
`user_id` int(32) NOT NULL,
`role_id` int(32) NOT NULL,
PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '2', '2');
2.3 项目结构
2.4 Maven依赖
<properties>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Shiro Spring-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2.5 基础代码
POJO、Mapper、Service等,不再这里赘述。
2.6 自定义Shiro Realm
/**
* 自定义权限控制数据源
*/
public class CustomRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
@Autowired
private IMenuService menuService;
/**
* 授权,通过服务加载用户角色和权限信息设置进去
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
if(null == principalCollection){
throw new AuthorizationException("principalCollection should not be null.");
}
//记录用户的角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//获取登录的用户名
String userName = (String) principalCollection.getPrimaryPrincipal();
//从数据库中,根据用户名查询用户信息
User user = userService.getUserByUserName(userName);
//用户所属角色集合
List<Role> roleList = roleService.queryByUserId(user.getUserId());
for (Role role : roleList) {
//添加角色
simpleAuthorizationInfo.addRole(role.getRoleName());
List<Menu> menuList = menuService.queryByRoleId(role.getRoleId());
//添加权限
for (Menu menu : menuList) {
simpleAuthorizationInfo.addStringPermission(menu.getPerms());
}
}
return simpleAuthorizationInfo;
}
/**
* 身份验证,通过服务加载用户信息并构造认证对象返回
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//从Token获取用户名,从主体认证信息中获取。Post请求时会先进入认证再到请求。
Object principal = authenticationToken.getPrincipal();
if(null == principal){
return null;
}
//获取用户名
String userName = String.valueOf(principal);
//获取密码
String password = new String((char[])authenticationToken.getCredentials());
//根据用户名查询用户信息
User user = userService.getUserByUserName(userName);
if(null == user){
throw new UnknownAccountException("用户名或密码错误!");
}
if(!user.getPassword().equals(MD5Util.md5Encrypt32Lower(password))){
throw new IncorrectCredentialsException("用户名或密码错误!");
}
//验证AuthenticationToken和SimpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo
= new SimpleAuthenticationInfo(userName,user.getPassword(),super.getName());
return simpleAuthenticationInfo;
}
}
2.7 自定义密码验证器
/**
* 自定义密码验证器
*/
public class CustomMatcher extends SimpleCredentialsMatcher {
/**
* 重写密码校验逻辑
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
String tokenPassword = this.encrypt(String.valueOf(usernamePasswordToken.getPassword()));
String password = (String)info.getCredentials();
return super.equals(tokenPassword,password);
}
/**
* 将传来的密码再次加密
* 可根据实际需求修改成别的加密方式
* @param str
* @return
*/
private String encrypt(String str){
return MD5Util.md5Encrypt32Lower(str);
}
}
2.8 自定义记住我过滤器
/**
* 自定义“记住我”过滤器
*/
public class CustomRememberMeFilter extends FormAuthenticationFilter {
/**
* 登录条件过滤:要么通过权限认证登录成功,要么通过“记住我”登录成功。
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = super.getSubject(request, response);
if(!subject.isAuthenticated() && !subject.isRemembered()){
if(null == subject.getSession().getAttribute("user") && null == subject.getPrincipal()){
subject.getSession().setAttribute("user",subject.getPrincipal());
}
}
return subject.isAuthenticated() || subject.isRemembered();
}
}
2.9 Shiro配置类
@Configuration
public class ShiroConfig {
/**
* 密码验证器
* @return
*/
@Bean
public CredentialsMatcher credentialsMatcher(){
return new CustomMatcher();
}
/**
* 权限验证
* @param credentialsMatcher
* @return
*/
@Bean
public CustomRealm customRealm(@Qualifier("credentialsMatcher") CredentialsMatcher credentialsMatcher){
CustomRealm customRealm = new CustomRealm();
//给权限验证器注入自定义的密码验证器
customRealm.setCredentialsMatcher(credentialsMatcher);
return customRealm;
}
/**
* 缓存管理
* @return
*/
@Bean
public CacheManager cacheManager(){
return new MemoryConstrainedCacheManager();
}
/**
* “记住我”Cookie管理
* @return
*/
@Bean
public CookieRememberMeManager cookieRememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
//手动设置对称加密密钥,避免出现 Unable to execute 'doFinal' with cipher instance 异常。原因在于:rememberMe的cookie在第二次打开页面后Shiro无法解密
cookieRememberMeManager.setCipherKey(Base64.decode("6ZmI6I2j3Y+R1aSn5BOlAA=="));
cookieRememberMeManager.setCookie(this.rememberMeCookie());
return cookieRememberMeManager;
}
/**
* “记住我”Cookie对象
* @return
*/
private SimpleCookie rememberMeCookie(){
SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//cookie有效时间为30天
simpleCookie.setMaxAge(2592000);
return simpleCookie;
}
/**
* 自定义“记住我”过滤器
* @return
*/
@Bean
public CustomRememberMeFilter customRememberMeFilter(){
return new CustomRememberMeFilter();
}
/**
* Session会话管理
* @return
*/
@Bean
public DefaultWebSessionManager sessionManager(){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
Long timeout=60L*1000*60;
defaultWebSessionManager.setGlobalSessionTimeout(timeout);
return defaultWebSessionManager;
}
/**
* SecurityManager安全管理器,起桥梁作用
* @param customRealm
* @return
*/
@Bean("securityManager")
public SecurityManager securityManager(@Qualifier("customRealm") CustomRealm customRealm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//自定义Realm
defaultWebSecurityManager.setRealm(customRealm);
//自定义CacheManager
defaultWebSecurityManager.setCacheManager(this.cacheManager());
//自定义“记住我”
defaultWebSecurityManager.setRememberMeManager(this.cookieRememberMeManager());
//自定义SessionManager
defaultWebSecurityManager.setSessionManager(this.sessionManager());
return defaultWebSecurityManager;
}
/**
* 全局配置,Filter工厂。
* 设置对应的过滤条件和跳转条件,有自定义的过滤器、Shiro认证成功、失败、退出登录等跳转的页面。
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("customRememberFilter",this.customRememberMeFilter());
LinkedHashMap<String,String> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("/logout","logout");
linkedHashMap.put("/error","anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(linkedHashMap);
return shiroFilterFactoryBean;
}
/**
* Shiro生命周期
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* 配置Shiro注解是否生效
* @RequiresRoles,@RequiresPermissions
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor sourceAdvisor = new AuthorizationAttributeSourceAdvisor();
sourceAdvisor.setSecurityManager(securityManager);
return sourceAdvisor;
}
}
2.10 登录Controller
@Controller
public class LoginController {
@Autowired
private IUserService userService;
@PostMapping("/login")
@ResponseBody
public String login(@RequestBody LoginDto loginDto){
UsernamePasswordToken uToken
= new UsernamePasswordToken(loginDto.getUserName(), loginDto.getPassword());
//记住我
uToken.setRememberMe(true);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(uToken);
}catch (UnknownAccountException e){
return e.getMessage();
}catch (IncorrectCredentialsException e){
return e.getMessage();
}
//这里可将一些用户信息保存在Session中
User user = userService.getUserByUserName(loginDto.getUserName());
subject.getSession().setAttribute("user",user);
return "登录成功!";
}
}
2.11 用户模块Controller
此处只演示用法,不赘述业务逻辑。具体业务逻辑可自行定义。
@RestController
@RequestMapping("/user")
public class UserController {
@RequiresPermissions("user:page")
@PostMapping("/page")
public String page(){
return "分页查询";
}
@RequiresPermissions("user:add")
@PostMapping("/add")
public String add(){
return "添加用户";
}
@RequiresPermissions("user:update")
@PostMapping("/update")
public String update(){
return "修改用户";
}
@RequiresPermissions("user:delete")
@PostMapping("/delete")
public String delete(){
return "删除用户";
}
}
2.12 Shiro异常处理
@ControllerAdvice
public class HandlerException {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String handlerShiroException(Exception e){
return "无权限操作!";
}
public String authorizationException(Exception e){
return "权限验证失败!";
}
}
三、演示
本案例的数据表中,设置了两个用户,两个角色,五个菜单。
用户名 | 角色 | 可操作菜单 |
admin | admin(系统管理员) | 用户管理:分页查询、添加、编辑、删除 |
biz | biz(业务用户) | 其他业务模块(不包括用户管理模块) |
验证规则:admin登录,可以调用page、add、update、delete接口;biz登录,
调用page、add、update、delete接口提示“无操作权限”。
3.1 admin登录
登录接口:
3.2 biz登录
登录接口:
四、笔记
关于CustomRealm里定义的两个方法,
doGetAuthorization方法用于授权,查询用户角色和权限信息,设置进AuthorizationInfo中;
doGetAuthentication方法,用于身份验证,查询用户信息,构造一个认证对象。
当我们调用“/login”接口,即调用subject.login(uToken)方法时,会自动进入CustomRealm中指定
doGetAuthentication(认证)方法,认证交由SimpleAuthenticationInfo来完成,它会完成身份密码的比较等功能。但是登录接口不走
doGetAuthorization(授权)方法。
当访问带有@RequiresPermissions或@RequiresRoles注解的接口时,才会去调用
doGetAuthorization(授权)方法。
五、参考链接
https://blog.csdn.net/jiankang66/article/details/90473517
https://www.cnblogs.com/Slags/p/12403448.html
https://www.jianshu.com/p/7f724bec3dc3
https://blog.csdn.net/lidai352710967/article/details/83654132
来源:oschina
链接:https://my.oschina.net/alexjava/blog/4466533