Shiro 是一个强大、简单易用的 Java 安全框架,主要用来更便捷的认证,授权,加密,会话管等等,可为任何应用提供安全保障。本篇主要来介绍 Shiro 的认证和授权功能。
Shiro 有三大核心的组件: Subject 、 SecurityManager 和 Realm 。它们之间的关系如下图所示:
1. Subject:认证主体。它包含两个信息:Principals 和 Credentials。
Principals:身份。可以是用户名,邮件,手机号码等等,用来标识一个登录主体身份;
Credentials:凭证。常见有密码,数字证书等等。
通俗来说,就是需要认证的东西,最常见的就是用户名密码了,比如用户在登录的时候,Shiro 需要去进行身份认证,就需要 Subject 认证主体。
2. SecurityManager:安全管理员。这是 Shiro 架构的核心,它就像 Shiro 内部所有原件的保护伞一样。在项目中一般都会配置 SecurityManager,主要是在 Subject 认证主体上面下功夫。
3. Realms:Realms 是一个域,它是连接 Shiro 和具体应用的桥梁,当需要与安全数据交互的时候,比如用户账户、访问控制等,Shiro 就会从一个或多个 Realms 中去查找。我们一般会自己定制Realm,下面详细说明。
org.apache.shiro
shiro-spring
1.4.0
用户表( user ,这里默认一个用户一个角色):
id | user_name | pwd | salt | role_id |
---|---|---|---|---|
1 | zhang_san | 123 | a1 | 1 |
3 | li_si | 789 | a3 | 2 |
角色表( role ):
role_id | role_code | role_name |
---|---|---|
1 | admin | 管理员 |
2 | user | 普通用户 |
权限表( auth ):
auth_id | auth_code | auth_name |
---|---|---|
1 | data:dele | 删除数据 |
2 | data:add | 添加数据 |
3 | data:get | 查询数据 |
4 | data:edit | 修改数据 |
role_auth 关联表
(这里默认 角色1管理员有全部权限,普通用户有“获取数据”和“添加数据”的权限)
id | role_id | auth_id |
---|---|---|
1 | 1 | 1 |
2 | 1 | 2 |
3 | 1 | 3 |
4 | 1 | 4 |
5 | 2 | 2 |
6 | 2 | 3 |
有了数据库表和数据之后,我们开始自定义 realm,自定义 realm 需要继承 AuthorizingRealm 类,因
为该类封装了很多方法,它也是一步步继承自 Realm 类的,继承了 AuthorizingRealm 类后,需要重写
两个方法:
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息
doGetAuthorizationInfo() 方法:用来为当前登陆成功的用户授予权限和角色
具体实现如下,相关的解释在代码的注释中,以便了解:
package com.tantela.config.shiro;
import com.tantela.dao.sys.UserMapper;
import com.tantela.model.sys.User;
import com.tantela.model.sys.UserShiro;
import com.tantela.service.sys.UserService;
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.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
/**
* @Author: ttllihao
* @Description: Realm 授权与认证配置
*/
public class RealmConfig extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private UserMapper userMapper;
/**
* 用户验证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upt = (UsernamePasswordToken) token;
// 根据token获取用户名
String username = upt.getUsername();
// 往数据库中查询用户
User user = userService.selectByUserName(username);
if (user == null) {
throw new UnknownAccountException();
}
Integer userId = user.getUid();
// 把当前用户 得 角色信息和权限信息存入会话
// 向数据库中查询该账户拥有的角色
List<String> roleList = userMapper.loginRole(userId);
List<String> authList = userMapper.loginAuth(userId);
user.setRoleList(roleList);// user类中有属性 List roleList
user.setAuthList(authList);// user类中有属性 List authList
// 把当前用户信息以及用户角色、权限存到ShiroSession中
SecurityUtils.getSubject().getSession().setAttribute(String.valueOf(userId),user);
// 传入用户名和密码进行身份认证,并返回认证信息
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
user,
//密码
user.getPassword(),
//ByteSource.Util.bytes(user.getCredentialsSalt()),// 盐
//getName()
""
);
return authenticationInfo;
}
/**
* 用户授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取认证时候添加到SimpleAuthenticationInfo中的实例
User user = (User) principals.getPrimaryPrincipal();
Integer userId = user.getUid();
// 将会话中得权限取出来。用作授权
Object object = SecurityUtils.getSubject().getSession().getAttribute(String.valueOf(userId));
User userShiro = (User)object;
// 将 List 转化为 Set,并实现去重
Set<String> roleSet = new HashSet<>(userShiro.getRoleList());
Set<String> authSet = new HashSet<>(userShiro.getAuthList());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.setRoles(roleSet);
authorizationInfo.setStringPermissions(authSet);
return authorizationInfo;
}
}
在上一步的认证和授权中,账号和密码是如何验证的呐?这里需要一个登录入口,告知用户输入的账号和密码。
@ResponseBody
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Map<String, Object> app(HttpServletRequest request, HttpSession session,
String username, String password, String code) {
Map<String, Object> map = new HashMap<>(8);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//下一步到Realm中认证
subject.login(token);
map.put("errMsg", "登陆成功");
map.put("success", true);
map.put("code", 0);
session.setAttribute("loginIp", sessionIp);
session.setAttribute("userName",username);
} catch (UnknownAccountException e) {
map.put("errMsg", MessageUtil.LOGIN_FAIL);
map.put("success", false);
map.put("code", 1);
} catch (IncorrectCredentialsException e) {
map.put("errMsg", "账号或密码不正确");
map.put("success", false);
map.put("code", 1);
} catch (LockedAccountException e) {
} catch (AuthenticationException e) {
return map;
}
@Configuration
public class ShiroConfig {
@Bean(name="shiroFilterFactoryBean")
public ShiroFilterFactoryBean shirFilter(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
// 未授权时跳转的提示界面
shiroFilterFactoryBean.setUnauthorizedUrl("/noAuth");
// 这里需要LinkedHashMap 不能HashMap (坑点之一:会出现代码已经配置却依然无权限访问的问题)
Map filterMap = new LinkedHashMap();
filterMap.put("/css/**","anon");
filterMap.put("/img/**","anon");
filterMap.put("/js/**","anon");
filterMap.put("/html/**","anon");
filterMap.put("/login/index","authc");
filterMap.put("/login/login","anon");
filterMap.put("/logout","logout");//配置退出 过滤器,其中的具体的退出代码Shiro已经实现
filterMap.put("/**","authc");//过滤链定义,从上向下顺序执行,一般将/**放在最为下边
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login/loginPage");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/system/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/system/noAuth");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean ;
}
/**
* 权限管理,配置主要是Realm的管理认证
*/
@Bean(name="securityManager")
public DefaultWebSecurityManager securityManager(@Qualifier("myShiroRealm")RealmConfig realm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
/**
* 将自己的验证方式加入容器
*/
@Bean(name="myShiroRealm")
public RealmConfig myShiroRealm(){
RealmConfig myShiroRealm = new RealmConfig();
//myShiroRealm.setCredentialsMatcher(hCM);
return myShiroRealm;
}
/**
* 开启shiro aop注解支持.(使用代理方式;所以需要开启代码支持;)
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorization(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
//凭证匹配器(由于密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
//@Bean(name="hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
}
说明:
认证过滤器:
anon:表示可以匿名使用。
authc:表示需要认证(登录)才能使用,没有参数
authcBasic:没有参数表示httpBasic认证
user:当登入操作时不做检查
授权过滤器
roles:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/=roles[“admin,guest”],每个参数通过才算通过,相当于hasAllRoles()方法。
perms:参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/=perms[“user:add:,user:modify:”],当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
rest:根据请求的方法,相当于/admins/user/*=perms[user:method] ,其中method为post,get,delete等。
port:当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
ssl:表示安全的url请求,协议为https