Shiro 提供了与 Web 集成的支持,其通过一个 ShiroFilter 入口来拦截需要安全控制的URL,然后进行相应的控制
ShiroFilter 类似于如 Strut2/SpringMVC 这种 web 框架的前端控制器,是安全控制的入口点,其负责读取配置(如ini 配置文件;springboot可使用注解配置类),然后判断URL 是否需要登录/权限等工作。
一、拦截请求链接
1.shiro提供了一系列的链接过滤器:
注:过滤器一般实现org.apache.shiro.web.filter.authc.AuthenticatingFilter类
2.注入Shiro拦截器工厂类(ShiroFilterFactoryBean),配置链接
拦截器类入口方法是createInstance(),该类的主要作用是:
一、 创建了FilterChainManager,即过滤器管理类,包括2个重要属性
1.1 filters:管理全部链接过滤器,包括身份验证的过滤器,有anon,authcBasic,auchc,user和权限验证的过滤器,有perms,roles,ssl,rest,port。同时自定的过滤器也在FilterChainManager里。值得注意的是,过滤器都是单例的。
1.2 filterChains:过滤链。是一个Map对象,其中key为请求的url,value是一个NamedFilterList对象,存放与该url对应的一系列过滤器
二、将过滤器管理类设置到PathMatchingFilterChainResolver类里,该类负责路径和过滤器链的解析与匹配。根据url找到过滤器链。
@Configuration
public class ShiroConfig {
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
* Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/Home");
// 未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
/**
* 拦截器.
* 定义shiro过滤链 Map结构
* Map中key(xml中是指value值)
*/
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断,优先匹配
filterChainDefinitionMap.put("/static/**", "anon");
//配置需要认证才能访问的链接
filterChainDefinitionMap.put("/**", "authc");
// 配置退出过滤器
filterChainDefinitionMap.put("/logout", "logout");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
}
}
3.请求链接解析
Shiro会替代org.springframework.web.filter.DelegatingFilterProxy来实现动态代理。DelegatingFilterProxy过滤器的代理类会实现拦截请求,任何请求都会先经过shiro先过滤,直到成功才会执行javaweb本身的过滤器。源码级讲解。
二、登录认证
1、首先调用Subject.login(token) 进行登录,其会自动委托给SecurityManager
2、SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator 进行身份验证;SecurityManager接口继承了Authenticator,另外还有一个ModularRealmAuthenticator实现,其委托给多个Realm 进行验证,验证规则通过AuthenticationStrategy接口指定
3、Authenticator 才是真正的身份验证者,ShiroAPI 中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator 的职责是验证用户帐号,是ShiroAPI 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的AuthenticationException异常
4、Authenticator 可能会委托给相应的AuthenticationStrategy进行多Realm 身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm 身份验证;
5、Authenticator 会把相应的token 传入Realm,从Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。
1.认证思路
程序先获当前用户的Subject对象,然后判断用户是否已经登录,如果登录则不用做认证,若没有登录,则创建 UsernamePasswordToken对象,将用户名密码传入Subject 的login对象进行检验。
@RequestMapping("/toLogin")
public String loginLogin(Model model, String username, String password, HttpSession userSession) {
// 判断用户名和密码是否为空
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
// 用户名或者密码为空
model.addAttribute("errorInfo", "用户名或者密码为空");
return "/login";
}
//通过subject进行登录操作
Subject subject = SecurityUtils.getSubject();
if(!subject.isAuthenticated()){
//创建封装了用户名和密码的UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);
User user = (User) subject.getPrincipal();
subject.getSession().setAttribute("user", user);
subject.getSession().setAttribute("userRole", userRole);
return "redirect:/Home";
} catch (AuthenticationException e) {
e.printStackTrace(); //打印异常错误
// 用户名或者密码为空
model.addAttribute("errorInfo", "用户名或者密码不正确");
return "/login";
}
}
else return "redirect:/Home";
}
2.深入探究
之前的简介中,已经知道了Realm就是shiro与数据库打交道的对象。
Shiro 从 Realm 获取安全数据(如用户、角色、 权限),即 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作。
先讲Realm的认证方面:
简单地说:**subject.login(token);**这句代码调用到最后,就是调用AuthenticationRealm抽象类中的抽象方法doGetAuthenticationInfo方法,doGetAuthenticationInfo方法会返回SimpleAuthenticationInfo对象,源码级讲解。而该方法就是上面的校验过程中,实现认证的自定义Realm需要实现的方法。我们可以通过继承该类,实现该方法达到自定义。
值得注意的是: 一般继承 AuthorizingRealm(授权)即可;其继承了 AuthenticatingRealm(即身份验证),而且也间接继承了 CachingRealm(带有缓存实现)
@Component
public class AuthRealm extends AuthorizingRealm{
@Autowired
private UserService userService;
@Override
//登陆认证模块
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
/*
* 为Shiro提供真实的用户数据
* 1.通过token获取用户名和密码
* 2.通过用户名和密码查询用户的真实的信息,真实的密码
* 3获取数据后通过info对象返回给shiro安全管理器
*/
//强转token为UsernamePasswordToken,才有getUsername等方法
//此处的token,就是subject.login(token)中的token
UsernamePasswordToken logintoken = (UsernamePasswordToken) token;
String username = logintoken.getUsername();
//通过用户名查询用户信息
User user = userService.finuserByUsername(username);
//密码的比对:
//通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!
//参数:user为数据库得到的对象,user.getUpassword()为数据库中的真实密码,this.getName()为当前的Realm名字,当验证通过时就返回,验证不通过就抛出异常。SimpleAuthenticationInfo还有其他参数,例如设置盐值加密。
AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getUpassword(), this.getName());
return info;
}
}
还有一点要强调的就是,shiro如何完成密码的比对?
我们知道此时,保存有用户信息的有UsernamePasswordToken和SimpleAuthenticationInfo两个对象,shiro肯定会去取出这两个对象中的信息进行比对。
简单地说:密码的具体比对工作是我们自定义的继承了AuthenticatingRealm父类的自定义
Realm类调用CredentialsMatcher的doCredentialsMatch方法完成的。源码级讲解。
3.多Realm认证
场景:假设某需求涉及使用两个角色分别是:学生、教师。要两者实现分开登录。即需要两个个Realm——StudentRealm和TeacherRealm,分别处理学生、教师的验证功能。
分析:正常情况下,当定义了多个Realm,无论是学生登录,教师登录,都会由这两个Realm共同处理。因为当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用哪个Realm的是doAuthenticate()方法,该方法中通过getRealms()获取Realm集合,如果realm只有一个,执行的是doSingleRealmAuthentication方法,如果有多个,走的是doMultiRealmAuthentication方法。所以当我们使用ModularRealmAuthenticator类来配置多个Realm的时候,Shiro会使用我们配置的多个Realm进行认证。
补充:modularRealmAuthenticator是shiro提供的realm管理器,在这里可以设置realm的生效。通过setAuthenticationStrategy来设置多realm的使用规则。如果想自己进一步控制多realm,可以自己实现ModularRealmAuthenticator 。
实现方法:创建一个org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类,并重写doAuthenticate()方法,让特定的Realm完成特定的功能。
1.通过创建一个org.apache.shiro.authc.UsernamePasswordToken的子类,在其中添加一个字段loginType,用来标识登录的类型,即是学生登录、教师登录。
enum LoginType {
STUDENT("Student"), TEACHER("Teacher")
private String type
private LoginType(String type) {
this.type = type
}
@Override
public String toString() {
return this.type.toString()
}
}
2.新建org.apache.shiro.authc.UsernamePasswordToken的子类UserToken
import org.apache.shiro.authc.UsernamePasswordToken
class UserToken extends UsernamePasswordToken {
//登录类型,判断是学生登录,教师登录
private String loginType
public UserToken(final String username, final String password,String loginType) {
super(username,password)
this.loginType = loginType
}
public String getLoginType() {
return loginType
}
public void setLoginType(String loginType) {
this.loginType = loginType
}
}
3.新建org.apache.shiro.authc.pam.ModularRealmAuthenticator的子类UserModularRealmAuthenticator:
/**
* 当配置了多个Realm时,我们通常使用的认证器是shiro自带的org.apache.shiro.authc.pam.ModularRealmAuthenticator,其中决定使用的Realm的是doAuthenticate()方法
*
* 自定义Authenticator
* 注意,当需要分别定义处理学生和教师和管理员验证的Realm时,对应Realm的全类名应该包含字符串“Student”“Teacher”。
* 并且,他们不能相互包含,例如,处理学生验证的Realm的全类名中不应该包含字符串"Teacher"。
*/
class UserModularRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger logger = LoggerFactory.getLogger(UserModularRealmAuthenticator.class)
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
logger.info("UserModularRealmAuthenticator:method doAuthenticate() execute ")
// 判断getRealms()是否返回为空
assertRealmsConfigured()
// 强制转换回自定义的CustomizedToken
UserToken userToken = (UserToken) authenticationToken
// 登录类型
String loginType = userToken?.getLoginType()
// 所有Realm
Collection<Realm> realms = getRealms()
// 登录类型对应的所有Realm
Collection<Realm> typeRealms = new ArrayList<>()
for (Realm realm : realms) {
if (realm?.getName()?.contains(loginType))
typeRealms?.add(realm)
}
// 判断是单Realm还是多Realm
if (typeRealms?.size() == 1){
logger.info("doSingleRealmAuthentication() execute ")
return doSingleRealmAuthentication(typeRealms?.get(0), userToken)
}
else{
logger.info("doMultiRealmAuthentication() execute ")
return doMultiRealmAuthentication(typeRealms, userToken)
}
}
}
4.创建分别处理学生登录和教师登录的StudentShiroRealm,TeacherShiroRealm (即自定义 Realm,这里自行编写代码):
5.在ShiroConfig类中的SecurityManager方法进行相应的配置,当然,以下只是ShiroConfig类中的少部分配置,还有属性的配置没有展示出来。
@Configuration
public class ShiroConfig {
@Bean
public SecurityManager securityManager(AuthRealm m) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置realm.
securityManager.setAuthenticator(modularRealmAuthenticator())
List<Realm> realms = new ArrayList<>()
//添加多个Realm
realms.add(teacherShiroRealm())
realms.add(studentShiroRealm())
securityManager.setRealms(realms)
return securityManager
}
/**
* 系统自带的Realm管理,主要针对多realm
* */
@Bean
public ModularRealmAuthenticator modularRealmAuthenticator(){
//自己重写的ModularRealmAuthenticator
UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator()
modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy())
return modularRealmAuthenticator
}
@Bean
public StudentShiroRealm studentShiroRealm() {
StudentShiroRealm studentShiroRealm = new StudentShiroRealm()
return studentShiroRealm
}
@Bean
public TeacherShiroRealm teacherShiroRealm() {
TeacherShiroRealm teacherShiroRealm = new TeacherShiroRealm()
return teacherShiroRealm
}
更多关于多Realm认证的细节,可以参考这位博主的文章。
三、授权(以注解为例)
1、首先调用Subject.isPermitted*/hasRole* 接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
2、Authorizer是真正的授权者,如果调用如isPermitted(“user:view”),其首先会通过 PermissionResolver把字符串转换成相应的Permission 实例;
3、在进行授权之前,其会调用相应的Realm 获取Subject 相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer 会判断Realm 的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted*/hasRole* 会返回true,否则返回false表示授权失败。
1.开启授权注解使用方式,在shiroconfig类中:
/**
* 开启授权注解使用的方式
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
2.在自定义的realm中实现授权方法
public class AuthRealm extends AuthorizingRealm{
@Autowired
private UserService userService;
@Override
//权限授权模块
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
List<String> listPermission = userService.findAdminRole(user.getUserId());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//授予admin权限
info.addRole("admin");
return info;
}
}
3.编写控制器(测试访问’/user/query’有权限,访问’/user/update’就没有权限)
@Controller
@RequestMapping("/user")
public class UserController{
@RequiresRoles(value = {admin})
@RequestMapping("/query")
public String query()
{
return "/user";
}
@RequiresRoles(value = {user})
@RequestMapping("/update")
public String query()
{
return "/user";
}
}
未完待续。。。