Apache Shiro
在 Java 领域一般有 Spring Security、 Apache Shiro 等安全框架,但是由于 Spring Security 过于庞大和复杂,大多数公司会选择 Apache Shiro 来使用。
Shiro 能做什么呢?
- 验证用户身份
- 用户访问权限控制,比如:1、判断用户是否分配了一定的安全角色。2、判断用户是否被授予完成某个操作的权限
- 在非 Web 或 EJB 容器的环境下可以任意使用 Session API
- 可以响应认证、访问控制,或者 Session 生命周期中发生的事件
- 可将一个或以上用户安全数据源数据组合成一个复合的用户 “view”(视图)
- 支持单点登录(SSO)功能
- 支持提供“Remember Me”服务,获取用户关联信息而无需登录
- …
关于Shiro更详细的介绍:Spring Boot (十四): Spring Boot 整合 Shiro-登录认证和权限管理
这里不再赘述关于Shiro的基本知识,只针对几个重点说明一下。
RBAC 基于角色的访问控制
主要涉及三个实体类:
- 用户信息
- 除了基本的用户名与密码外,增加一个用于加密的盐。
- 用@ManyToMany与角色信息表联结。
- 角色信息
- 角色唯一标识(String类型)
- 用@ManyToMany与权限信息表联结。
- 权限信息
- 权限唯一标识(String类型)
- 当前权限可访问的URL(不是必要的)
Shiro配置
@Configuration
public class ShiroConfig {
/**
* Shiro过滤器
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
/*
* 配置URL
*/
shiroFilterFactoryBean.setLoginUrl("/login"); // 登录界面
shiroFilterFactoryBean.setSuccessUrl("/index"); // 登录成功跳转页面
shiroFilterFactoryBean.setUnauthorizedUrl("/403"); // 权限不足跳转页面
/*
* 拦截器(从上往下顺序执行)
* anon:所有URL可匿名访问
* authc:需要认证才可访问
* user:配置记住我或认证通过可访问
*/
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/static/**", "anon");
filterChainDefinitionMap.put("/logout", "logout"); //退出过滤器,Shiro封装好了退出业务
filterChainDefinitionMap.put("/**", "authc"); //其他资源全部需要认证,该过滤器放在最后,不会覆盖之前的过滤器
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器 加密配置
* 由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true); //true时用Hex编码,false时用Base64编码
return hashedCredentialsMatcher;
}
/**
* 告诉realm,使用credentialsMatcher加密算法类来验证密文
* @return
*/
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name="simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError");//数据库异常处理
mappings.setProperty("UnauthorizedException","403");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
//r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
}
Shiro登录认证实现
doGetAuthorizationInfo如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。
doGetAuthenticationInfo所做的就是根据用户输入的用户名,去数据库查出该用户的加密密码与盐,再将用户输入的密码按照同样方法加密,与正确的加密密码比较是否相等。
public class MyShiroRealm extends AuthorizingRealm {
// 用户对应的角色信息与权限信息都保存在数据库中,通过UserService获取数据
@Resource
private UserInfoService userInfoService;
/**
* 权限认证
* 提供用户信息返回权限信息
* @param principals 用户信息
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
for(SysRole role : userInfo.getRoleList()){
authorizationInfo.addRole(role.getRole()); // 将角色名称提供给info
for(SysPermission p : role.getPermissions()){
authorizationInfo.addStringPermission(p.getPermission()); // 将权限名称提供给info
}
}
return authorizationInfo;
}
/**
* 身份认证
* 提供账户信息返回认证信息
* @param token 令牌
* @return
* @throws AuthenticationException 权限异常,交给Controller处理
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
//获取用户的输入的账号
String username = (String)token.getPrincipal();
System.out.println(token.getCredentials());
//通过username从数据库中查找 User对象
//实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userInfoService.findByUsername(username);
System.out.println("----->>userInfo="+userInfo);
if (userInfo == null) {
return null;
}
if (userInfo.getState() == UserInfo.StateEnum.LOCKED) {
throw new LockedAccountException(); //用户被锁定抛出异常
} else if (userInfo.getState() == UserInfo.StateEnum.UNACTIVATED) {
throw new DisabledAccountException(); //用户未激活异常
}
// 交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
userInfo, //用户名
userInfo.getPassword(), //密码
ByteSource.Util.bytes(userInfo.getCredentialsSalt()),//salt=username+salt
getName() //realUsernamePasswordToken m name
);
return authenticationInfo;
}
}
散列算法与加密算法
md5是本文会使用的散列算法,加密算法本文不会涉及。散列和加密本质上都是将一个Object变成一串无意义的字符串,不同点是经过散列的对象无法复原,是一个单向的过程。例如,对密码的加密通常就是使用散列算法,因此用户如果忘记密码只能通过修改而无法获取原始密码。但是对于信息的加密则是正规的加密算法,经过加密的信息是可以通过秘钥解密和还原。