前言
这篇文章来学习如何使用 Spring Boot 集成 Apache Shiro 。安全应该是互联网公司的一道生命线,几乎任何的公司都会涉及到这方面的需求。在 Java 领域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 过于庞大和复杂,并且依赖spring环境;而Apache Shiro就相对独立,最主要是因为shiro使用简单、灵活,所以现在越来越多的用户选择Apache Shiro。这篇文章会先介绍一下 Apache Shiro ,在结合 Spring Boot 给出使用案例。
Apache Shiro
它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
快速上手
1. 添加相关依赖
<!-- spring 集成 shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
RBAC
RBAC 是基于角色的访问控制(Role-Based Access Control )在 RBAC 中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
2. 创建相关系统表
用户信息表:
DROP TABLE IF EXISTS `account`;
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`rid` int(11) NOT NULL DEFAULT '2' COMMENT '角色id',
`username` varchar(32) NOT NULL COMMENT '用户名',
`password` varchar(32) NOT NULL COMMENT '密码',
`salt` varchar(32) NOT NULL COMMENT '盐值',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
角色信息表:
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` varchar(32) NOT NULL COMMENT '角色名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
权限信息表:
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`name` varchar(32) NOT NULL COMMENT '权限名称',
`url` varchar(32) NOT NULL COMMENT '权限url',
`type` varchar(32) NOT NULL COMMENT '权限类型',
`percode` varchar(32) DEFAULT NULL COMMENT '权限标识',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
角色-权限信息表:
DROP TABLE IF EXISTS `role_permission`;
CREATE TABLE `role_permission` (
`rid` int(11) NOT NULL COMMENT '角色id',
`pid` int(11) NOT NULL COMMENT '权限id'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
用户权限视图:
select
`a`.`id` AS `id`,`p`.`name` AS `name`,`p`.`percode` AS `percode`
from
`account` `a`
join
`role` `r`
on
`a`.`rid` = `r`.`id`
join
`role_permission` `rp`
on
`r`.`id` = `rp`.`rid`
join
`permission` `p`
on
`rp`.`pid` = `p`.`id`
3. Shiro 配置
ShiroConfig.java
@Configuration
public class ShiroConfig {
/**
* 1. 凭证匹配器
*
* @return
*/
@Bean("credentialsMatcher")
public HashedCredentialsMatcher getCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 2. 自定义realm
*
* @return
*/
@Bean("shiroUserRealm")
public ShiroUserRealm getShiroUserRealm() {
ShiroUserRealm shiroUserRealm = new ShiroUserRealm();
shiroUserRealm.setCredentialsMatcher(getCredentialsMatcher());
return shiroUserRealm;
}
/**
* 3.1 设置记住我cookie
* @return
*/
@Bean
public SimpleCookie getSimpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setMaxAge(60*60*24*7);
simpleCookie.setName("rememberMe");
return simpleCookie;
}
/**
* 3.2 配置记住我
* @return
*/
@Bean
public CookieRememberMeManager getCookieRememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(getSimpleCookie());
return cookieRememberMeManager;
}
@Bean
public Authorizer authorizer(ShiroUserRealm shiroUserRealm) {
ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();
Collection<Realm> crealms = new ArrayList<>();
crealms.add(shiroUserRealm);
authorizer.setRealms(crealms);
return authorizer;
}
@Bean
public Authenticator authenticator(ShiroUserRealm shiroUserRealm) {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
Collection<Realm> crealms = new ArrayList<>();
crealms.add(shiroUserRealm);
authenticator.setRealms(crealms);
return authenticator;
}
/**
* 4. securityManager管理器
*
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager getSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
ShiroUserRealm shiroUserRealm = getShiroUserRealm();
securityManager.setAuthenticator(authenticator(shiroUserRealm));
securityManager.setAuthorizer(authorizer(shiroUserRealm));
securityManager.setRealm(shiroUserRealm);
securityManager.setRememberMeManager(getCookieRememberMeManager());
return securityManager;
}
/**
* 5. shiroFilter
* @param securityManager
* @return
*/
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/api/login");
// 登录成功后要跳转的链接
//shiroFilterFactoryBean.setSuccessUrl("/api/index");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/login.html");
// 拦截器链
Map<String, String> map = new LinkedHashMap<String, String>();
map.put("/toLogin", "anon");
map.put("/api/login", "authc");
map.put("/api/logout", "logout");
map.put("/admin/**", "user");
map.put("/user/**", "user");
map.put("/js/**", "anon");
map.put("/css/**", "anon");
map.put("/images/**", "anon");
map.put("/**", "anon");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
return shiroFilterFactoryBean;
}
/**
* 开启shiro aop注解支持. 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 开启Shiro的注解
* (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 配置authc,不是必须配置,如果不配置,表单的用户名和密码必须是:username,password
*
* @return
*/
@Bean("formAuthenticationFilter")
public FormAuthenticationFilter getFormAuthenticationFilter() {
FormAuthenticationFilter formAuthenticationFilter = new FormAuthenticationFilter();
formAuthenticationFilter.setUsernameParam("username");
formAuthenticationFilter.setPasswordParam("password");
return formAuthenticationFilter;
}
}
4. 自定义ShiroUserRealm
public class ShiroUserRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private AccountService accountService;
@Autowired
private PermissionService permissionService;
/**
* 验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = token.getPrincipal().toString();
logger.info("----------认证-----------------username="+username);
AccountEntity accountEntity = accountService.getByUsername(username);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(accountEntity, accountEntity.getPassword(),ByteSource.Util.bytes(accountEntity.getSalt()),getName());
return simpleAuthenticationInfo;
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
AccountEntity accountEntity = (AccountEntity) principal.getPrimaryPrincipal();
logger.info("----------凭证-----------------password="+accountEntity.getPassword());
List<Permission> permissions = permissionService.getByUserId(accountEntity.getId());
if(null == permissions || permissions.size()==0) {
return null;
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
for (Permission permission : permissions) {
String percode = permission.getPercode();
if(null!=percode) {
info.addStringPermission(percode);
}
}
return info;
}
/**
* 清理缓存
*/
protected void clearCache() {
Subject subject = SecurityUtils.getSubject();
super.clearCache(subject.getPrincipals());
}
}
5. 登录实现:
/**
* 登录
*
* @param req
* @return
*/
@ResponseBody
@PostMapping("/api/login")
public Result login(HttpServletRequest request, String username, String password,String captcha,Boolean rememberMe){
if(StringUtils.isBlank(username) || StringUtils.isBlank(password)|| StringUtils.isBlank(captcha) ) {
logger.info("参数错误");
return Result.fail("参数错误...");
}
String captchaCookie = CookieUtil.getCookieByName(request, "shiro_captcha");
if(StringUtils.isBlank(captchaCookie)) {
logger.info("没有验证码");
return Result.fail("没有验证码...");
}
Object selCaptcha = CacheUtils.get(captchaCookie);
CacheUtils.remove(captchaCookie);
if(null == selCaptcha ) {
logger.info("验证码失效");
return Result.fail("验证码失效...");
}
if(!captcha.equalsIgnoreCase(selCaptcha.toString())) {
logger.info("验证码错误");
return Result.fail("验证码错误...");
}
UsernamePasswordToken token = new UsernamePasswordToken(username,password,rememberMe);
Subject subject = SecurityUtils.getSubject();
try {
subject.login(token);
return Result.success("登录成功");
} catch (Exception e) {
String exceptionClassName = (String)request.getAttribute("shiroLoginFailure");
String msg = "用户名/密码错误";
if(UnknownAccountException.class.getName().equals(exceptionClassName)) {
return Result.fail(msg);
} else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)){
return Result.fail(msg);
}
return Result.fail(exceptionClassName);
}
}