Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理
由于项目需求,自学了Shiro框架,本文记录一下所用到的技术和一些无法理解的坑。
其中Shiro框架包含了很多bean属性,这些bean是可以通过自定义的方式更加符合开发需求,开发者可以通过@Configuration注释重写ShiroConfig配置文件,来设置自己需要用到的bean,上代码
package com.xfs.admin.web.config;
import com.xfs.admin.web.shiro.ShiroUtils;
import com.xfs.admin.web.shiro.UserRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
private static final String anon = "anon"; // 匿名用户可访问
private static final String authc = "authc"; // 认证用户可访问
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName(ShiroUtils.HASH_ALGORITHM);
credentialsMatcher.setHashIterations(ShiroUtils.HASH_ITERATIONS);
credentialsMatcher.setStoredCredentialsHexEncoded(true);
return credentialsMatcher;
}
@Bean
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
userRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return userRealm;
}
private static final String COOKIE_NAME = "rememberMe";
/** cookie对象管理 */
public SimpleCookie rememberMeCookie(){
SimpleCookie simpleCookie = new SimpleCookie(COOKIE_NAME);
simpleCookie.setMaxAge(604800);//记住我cookie生效时间7天 ,单位秒
return simpleCookie;
}
/** cookie管理对象 : 记住我功能 */
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
return cookieRememberMeManager;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setCacheManager(new EhCacheManager());
//注入记住我管理器;
securityManager.setRememberMeManager(rememberMeManager());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager());
shiroFilter.setLoginUrl("/login.html");
shiroFilter.setUnauthorizedUrl("/");
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/AdminLTE30/**", anon);
filterMap.put("/images/**", anon);
filterMap.put("/captcha", anon);
filterMap.put("/login.html", anon);
filterMap.put("/login", anon);
// 用户页面的权限
filterMap.put("/user/delete", "perms[/user/delete]");
filterMap.put("/user/list", "perms[/user/list]");
filterMap.put("/user/edit", "perms[/user/edit]");
filterMap.put("/user/add", "perms[/user/add]");
// 日志页面的权限
filterMap.put("/tasklog/list", "perms[/tasklog/list]");
filterMap.put("/tasklog/add", "perms[/tasklog/add]");
filterMap.put("/tasklog/delete", "perms[/tasklog/delete]");
filterMap.put("/tasklog/edit", "perms[/tasklog/edit]");
filterMap.put("/**", authc);
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
}
简单解释一下这个文件
比如:hashedCredentialsMatcher 定义了密码的编码加密方式,这里隐藏了。
securityManager 定义了安全管理器,按需要注入自定义的realm类(自定义doGetAuthorizationInfo方法及doGetAuthenticationInfo方法)、cookie管理对象(登录页面的“记住我”功能)、session管理对象(查询数据库后的数据缓存)
advisorAutoProxyCreator 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
authorizationAttributeSourceAdvisor 开启aop注解支持
shiroFilter定义了URL的访问规则(设置登录页、没有权限支持返回页)
------------------------分割一下-------------------------
那么设置好配置类,怎么调用呢?既然说到了自定义的realm类,那就重写一下试试,so…
package com.xfs.admin.web.shiro;
import com.xfs.admin.web.config.ShiroConfig;
import com.xfs.admin.web.entity.Role;
import com.xfs.admin.web.entity.User;
import com.xfs.admin.web.service.RoleService;
import com.xfs.admin.web.service.UserService;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Autowired
RoleService roleService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Integer userId = user.getId();
List<String> permList = userService.queryAllPerms(userId);
Set<String> permSet = new HashSet<String>();
for (String perm : permList) {
if (StringUtils.isBlank(perm)) {
continue;
}
permSet.add(perm);
}
if(permSet.size() != 0){
info.setStringPermissions(permSet);
}
return info;
}
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
User user = userService.selectByLoginName(token.getUsername());
if (null == user) {
throw new UnknownAccountException("账号或密码不正确");
}
if (user.getStatus() == User.UserStatus.LOCKED.getCode()) {
throw new LockedAccountException("账号已被锁定,请联系管理员");
}
ByteSource byteSource = ByteSource.Util.bytes(user.getSalt());
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), byteSource, getName());
return info;
}
@Override
public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.HASH_ALGORITHM);
shaCredentialsMatcher.setHashIterations(ShiroUtils.HASH_ITERATIONS);
super.setCredentialsMatcher(shaCredentialsMatcher);
}
}
一定一定要继承AuthorizingRealm 类,很重要!!!
这个类的doGetAuthorizationInfo方法是需要一定的条件才可以触发,而doGetAuthenticationInfo方法是框架给你的简单的登录验证方法,其中的user是要在数据库查数据来进行对比才可以验证通过,而数据库其中的password是加密后的,也就是说在页面登录输入的明码是在这个方法加密后去和数据库拿到的密码String.equals的,而上面提到的加密方式正是这里需要的。
-------------------------重点来了。。。再割一下------------------------
doGetAuthorizationInfo方法的触发正是实现权限控制的重中之重。
而触发doGetAuthorizationInfo方法有两种方式:
一、通过Shiro的注解(如@RequiresRoles,@RequiresPermissions)
@RequiresRoles通过角色控制 @RequiresPermissions通过权限控制
使用在Controller中,位置在@RequestMapping上添加例如@RequiresPermissions({"/user/add"} )
当出现注释时,Shiro启动配置好的自定义的realm类中doGetAuthorizationInfo方法,该方法返回的SimpleAuthorizationInfo可调用setStringPermissions方法及setRoles方法添加数据库查询出的权限和角色,然后和注释里的进行比较
当需要多个权限才可访问时,可设置@RequiresRoles(value={“user”,“admin”},logical=Logical.OR)
Logical.OR表示角色为user或者admin才可访问,AND表示同时拥有两者才可访问,也可不设置,
拥有权限>>运行页面,否则>>返回一个叫UnauthorizedException的异常,这个异常是需要自定义全局异常处理的,比如
@ExceptionHandler({UnauthorizedException.class})
public String unException (UnauthorizedException e){
return "没有权限,请联系管理员";
}
二、通过shiroFilter的Bean
使用ShiroFilterFactoryBean.setFilterChainDefinitionMap方法需要传入map,而使用new LinkedHashMap<>()是保证扫描的先后顺序,Shiro会从上到下,从左到右扫描
当有perms或roles(如"/user/**", “roles[user]”)出现时,调用doGetAuthorizationInfo方法
这个方法是无法设置多个权限的,也就是一对一的权限设置,而且当权限不通过时会找到setUnauthorizedUrl设置的页面,我这里是选择了重新登录。
这样就完成了
参考文献
https://blog.csdn.net/kity9420/article/details/89067794
https://blog.csdn.net/qi923701/article/details/75224554?utm_source=blogxgwz2