- 安全时互联网公司的一道生命线,几乎所有公司都会涉及到这方面的需求。在Java领域一般有Spring Security、Apache Shiro等安全框架,但是由于Spring Security过于庞大和复杂,大多数公司选择Apache Shiro来使用。
- Apache Shiro是一个功能强大、灵活的、开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。Apache Shiro的首要目标是易于使用和理解。安全通常很复杂,甚至让人感到痛苦,但是Shiro却不是这样子的。一个好的安全框架应该是屏蔽复杂性,向外暴露简单、直观的API来简化开发人员实现应用程序安全所花费的时间和精力。
Shiro能做什么呢?
- 验证用户身份
- 用户访问权限控制,比如:1.判断用户是否分配了一定的安全角色。2.判断用户是否被授予完成某个操作的权限
- 在非Web或EJB容器的环境下可以任意使用Session API
- 可以响应认证、访问控制、或则Session生命周期中发生的事件
- 可以将一个或以上用户安全数据源数据组合成一个复合的用户“view(视图)”
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
等都集成到一个有凝聚力的易于使用的API。Shiro致力在所有应用环境下实现上诉功能,小到命令行应用程序,大到企业应用中,而且不需要借助第三方框架、容器、应用服务器等。当然Shiro的目的是尽量的融入到这样的应用环境中去,但也可以在它们之外的任何环境下开箱即用。
Shiro特性
- 是一个全面的、蕴含丰富功能的安全框架。
- Authentication(认证),Authorization(授权),Session Management(会话管理),Cryptography(加密)被Shiro框架的开发团队称之为应用安全的四大基石。
- Authentication(认证):用户身份识别,通常被称为用户“登录”
- Authorization(授权):访问控制,比如某个用户是否具有某个操作的权限
- Session Management(会话管理):特定于用户的会话管理,甚至在非web或EJB应用程序
- Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用
它还有其他功能来支持和加强这些不同应用环境下安全领域的关注点。
- Web支持:Shiro提供Web支持api,可以很轻松保护Web应用程序的安全
- 缓存:缓存Apache Shiro保证安全操作快速、高效的重要手段
- 并发:Apache Shiro支持多线程应用程序的并发特性
- 测试:支持单元测试和集成测试,确保代码和预想的一样安全
- “Run As”:这个功能允许用户假设另一个用户的身份(在许可的前提下)
- “Remember Me”:跨session记录用户的身份,只有在强制需要时才需登录
Shiro不会去维护用户、维护权限、这些需要我们自己去涉及/提供,之后通过相应的接口注入给Shiro
pom文件引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>1.2.1</version>
</dependency>
项目中预先准备用户表,角色表,用户角色表,权限菜单表,角色权限菜单表
/*
Navicat MySQL Data Transfer
Source Server : 本机mysql
Source Server Version : 50713
Source Host : localhost:3306
Source Database : zmx
Target Server Type : MYSQL
Target Server Version : 50713
File Encoding : 65001
Date: 2019-09-03 15:43:05
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`user_id` bigint(20) NOT NULL,
`avatar` varchar(255) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`phone` varchar(255) DEFAULT NULL,
`realname` varchar(255) DEFAULT NULL,
`sex` varchar(255) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES ('1', null, null, '123456', null, null, null, 'admin');
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`role_id` bigint(20) NOT NULL,
`role_name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES ('2', '管理员');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_role_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
`user_id` bigint(20) NOT NULL,
PRIMARY KEY (`user_role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES ('3', '2', '1');
-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`menu_id` bigint(20) NOT NULL,
`permission` varchar(255) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL,
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES ('4', '所有权限', '/test');
-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`role_menu_id` bigint(20) NOT NULL,
`menu_id` bigint(20) NOT NULL,
`role_id` bigint(20) NOT NULL,
PRIMARY KEY (`role_menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES ('5', '4', '2');
INSERT INTO `role_menu` VALUES ('6', '4', '2');
接着准备配置文件
1.添加Realm验证
Shiro架构包含了三个主要的理念:Subject,SecurityManager和Realm。
- Subject:当前用户,Subject可以是一个人,也可以时第三方服务、守护进程账户,时钟守护任务或者其它--当前和软件交互的任何事件
- SecurityManager:管理所有Subject,SecurityManager是Shiro架构的核心,配合内部安全组件共同组成安全伞
- Realms:用于进行权限信息的验证,我们自己实现。Realm本质上是一个特定的安全DAO:它封装与数据源连接的细节,得到Shiro所需的相关的数据。在配置Shiro的时候,必须指定至少一个Realm来实现认证和授权
我们需要实现Realms的Authentication和Authorization。其中Authentication是用来验证用户身份的,Authorization是授权访问控制,用于对用户进行操作授权,证明该用户是否允许进行当前操作,例如访问某个链接,某个资源等
package com.springboot_springdatajpa.springdatajpa.config;
import com.springboot_springdatajpa.springdatajpa.model.MenuModel;
import com.springboot_springdatajpa.springdatajpa.model.RoleModel;
import com.springboot_springdatajpa.springdatajpa.model.UserModel;
import com.springboot_springdatajpa.springdatajpa.repository.MenuDAO;
import com.springboot_springdatajpa.springdatajpa.repository.UserRepository;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* @Author zhaomengxia
* @create 2019/9/3 10:06
*/
public class ShiroRealm extends AuthorizingRealm {
private Logger logger= LoggerFactory.getLogger(this.getClass());
@Autowired
private UserRepository userRepository;
@Autowired
private MenuDAO menuDAO;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info("doGetAuthorizationInfo"+principalCollection.toString());
UserModel userModel=userRepository.findByUsername((String) principalCollection.getPrimaryPrincipal());
//把principals放session中,key=userId value=principals
SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(userModel.getUserId()),SecurityUtils.getSubject().getPrincipals());
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
List<RoleModel> roleModels=menuDAO.findByUserID(userModel.getUserId());
for (RoleModel roleModel:roleModels){
info.addRole(roleModel.getRoleName());
}
List<MenuModel> menuModels=menuDAO.findByUserId(userModel.getUserId());
for (MenuModel menuModel:menuModels){
info.addStringPermission(menuModel.getPermission());
}
return info;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
logger.info("doGetAuthenticationInfo"+authenticationToken.toString());
UsernamePasswordToken token= (UsernamePasswordToken) authenticationToken;
String userName=token.getUsername();
logger.info(userName+token.getPassword());
UserModel userModel=userRepository.findByUsername(userName);
if (userModel!=null){
Session session=SecurityUtils.getSubject().getSession();
session.setAttribute("userModel",userModel);
return new SimpleAuthenticationInfo(userName,userModel.getPassword(),getName());
}
else{
return null;
}
}
}
2.添加shiro配置
package com.springboot_springdatajpa.springdatajpa.config;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.filter.authc.LogoutFilter;
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.springframework.context.annotation.DependsOn;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author zhaomengxia
* @create 2019/9/3 9:54
*/
@Configuration
public class ShiroConfiguration {
/**
* LifecycleBeanPostProcessor,这是个DestructionAwareBeanPostProcessor的子类
* 负责org.apache.shiro.util.Initializable类型bean的生命周期的初始化和销毁
* 主要是AuthorizingRealm类的子类,以及EhCacheManager类
* @return
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
return new LifecycleBeanPostProcessor();
}
/**
* HashedCredentialsMatcher这个是为了对密码进行编码的,防止密码在数据库里明码保存,当然在登陆认证的时候,
* 这个类也负责对form里输入的密码进行编码。
* @return
*/
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher credentialsMatcher=new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(2);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
/**
* ShiroRealm这个是自定义的认证类,继承自AuthorizingRealm,负责用户的认证和权限的处理,可以参考JdbcRealm的实现
* @return
*/
@Bean(name = "shiroRealm")
@DependsOn("lifecycleBeanPostProcessor")
public ShiroRealm shiroRealm(){
ShiroRealm realm=new ShiroRealm();
// realm.setCredentialsMatcher(hashedCredentialsMatcher());
return realm;
}
/**
* EhCacheManager缓存管理,用户登录成功后,把用户信息和权限信息缓存起来
* 然后每次用户请求时,放入用户的session中,如果不设置这个bean,每个请求都会查询一次数据库
* @return
*/
@Bean(name = "ehCacheManager")
@DependsOn("lifecycleBeanPostProcessor")
public EhCacheManager ehCacheManager(){
return new EhCacheManager();
}
/**
* DefaultWebSecurityManager权限管理,这个类组合了登陆,登出,权限,session的处理,时比较重要的类
* @return
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm());
securityManager.setCacheManager(ehCacheManager());
return securityManager;
}
/**
* ShiroFilterFactoryBean,是个factorybean,为了生成ShiroFilter。
* 它主要保持了三项数据,securityManager,filters,filterChainDefinitionManager。
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager());
Map<String, Filter> filters = new LinkedHashMap<>();
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/login");
// filters.put("logout",null);
shiroFilterFactoryBean.setFilters(filters);
Map<String, String> filterChainDefinitionManager = new LinkedHashMap<String, String>();
filterChainDefinitionManager.put("/logout", "logout");
filterChainDefinitionManager.put("/user/**", "authc,roles[ROLE_USER]");//用户为ROLE_USER 角色可以访问。由用户角色控制用户行为。
filterChainDefinitionManager.put("/events/**", "authc,roles[ROLE_ADMIN]");
// filterChainDefinitionManager.put("/user/edit/**", "authc,perms[user:edit]");// 这里为了测试,固定写死的值,也可以从数据库或其他配置中读取,此处是用权限控制
filterChainDefinitionManager.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
shiroFilterFactoryBean.setSuccessUrl("/");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
return shiroFilterFactoryBean;
}
/**
* DefaultAdvisorAutoProxyCreator,Spring的一个bean,由Advisor决定对哪些类的方法进行AOP代理。
*/
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
/**
* AuthorizationAttributeSourceAdvisor,shiro里实现的Advisor类,
* 内部使用AopAllianceAnnotationsAuthorizingMethodInterceptor来拦截用以下注解的方法。
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
aASA.setSecurityManager(securityManager());
return aASA;
}
}
LoginController.java
package com.springboot_springdatajpa.springdatajpa.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
/**
* 用户接口,用于测试的接口
* 这里使用了标准的restful接口的风格,swagger自动的API接口,shiro接口权限注解
* @RequiresPermissions组合成的一个controller。当然也可以使用其他技术,只要能够获取到接口信息就行
* @Author zhaomengxia
* @create 2019/9/3 11:26
*/
@RestController
@Api(tags = "登录")
@RequestMapping("/test")
public class LoginController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@RequestMapping(value = "/login", method = RequestMethod.POST)
@ApiOperation("登录接口")
@RequiresPermissions("user:list")
public String login(
@RequestParam(value = "username", required = true) String userName,
@RequestParam(value = "password", required = true) String password,
@RequestParam(value = "rememberMe", required = true, defaultValue = "false") boolean rememberMe
) {
logger.info("==========" + userName + password + rememberMe);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(userName, password);
token.setRememberMe(rememberMe);
try {
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
// rediect.addFlashAttribute("errorText", "您的账号或密码输入错误!");
return "{\"Msg\":\"您的账号或密码输入错误\",\"state\":\"failed\"}";
}
return "{\"Msg\":\"登陆成功\",\"state\":\"success\"}";
}
@RequestMapping("/")
@ResponseBody
@RequiresPermissions("user:get")
@ApiOperation(value = "测试")
public String index() {
return "no permission";
}
}
源代码