前言
之前写项目用了 Shiro 框架,来进行安全验证以及权限管理。当时项目赶得急,没怎么深入了解,只能说能跑能改,不过在使用的过程中发现 Shiro 确实很优秀。现在回过头来学习原理,读读源码,深入的学习下。·
本篇博文主要写的是关于使用 Shiro 起步时最重要的一块,找了一些资料,力求写得简单明了。
简介
Realm:域,Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro 。当配置 Shiro时,你必须至少指定一个 Realm ,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。
Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。
功能
Realm能做的工作主要有以下几个方面:
身份验证(getAuthenticationInfo 方法)验证账户和密码,并返回相关信息
权限获取(getAuthorizationInfo 方法) 获取指定身份的权限,并返回相关信息
令牌支持(supports方法)判断该令牌(Token)是否被支持
令牌有很多种类型,例如:HostAuthenticationToken(主机验证令牌),UsernamePasswordToken(账户密码验证令牌)
这里主来说明一下关于前两点验证方面的逻辑,因为令牌一般用的都是 UsernamePasswordToken,哪怕用 HostAuthenticationToken,也没必要细讲,这个函数很少用到。
身份验证
我们看到第一个方法就是我们上面说的“验证账户和密码,并返回相关信息”的方法。从方法的名字上看,只有取得验证信息的意思,其实这里面还包括了进行验证的逻辑。
看Javadoc,这个方法的作用是:根据传进来的 Token,返回用户的验证信息。下面说明一下 Token 和 用户验证信息 。
Token:就是要拿来进行验证的信息,例如:如果是 UsernamePasswordToken 的话,这个 Token 的内容就是“用户提交的用户名和密码”。
来看下 UsernamePasswordToken 的属性。
1 2 3 4 5 6 |
public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken { private String username; private char[] password; private boolean rememberMe; private String host; ... |
用户验证信息:就是用户验证通过后,返回给系统的信息。例如:用户登录验证的话,一般来说,返回给系统的“用户验证信息”就应该是这个用户的“用户名和密码”。但也可以返回其它信息,例如返回用户的“邮箱地址和登录密码”信息,做为“用户验证信息”。 那么返回给谁呢,Shiro 中的三大组件之一的 Subject。
不细谈,这么说吧,Subject:即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是 Shiro 的“用户”概念。
上面说了“根据传进来的Token”和“返回用户的验证信息”,但没有说验证的过程,这个过程也是在这个方法中进行。我们看一下源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); // doGetAuthenticationInfo方法的内容,由各个子类来实现。 // 主要是用来取得我们保存的“用户验证信息”,例如DB里保存的密码(具体看JdbcRealm的方法实现) if (info == null) { info = doGetAuthenticationInfo(token); ... } // 在这里,把用户提交的信息(Token)和我们保存的“用户验证信息”进行比较 // 如果不通过,直接抛出定义好的异常。 if (info != null) { assertCredentialsMatch(token, info); } else { return info; } |
权限获取
“权限验证”的处理,是由接口定义的。但“验证是否有访问权限”的逻辑,则是由类定义的。定义的类为:AuthorizingRealm ,在这个类中有个getAuthorizationInfo 方法。这个方法和getAuthenticationInfo 方法的处理流程有点像:
验证是否有指定的权限
返回用户的权限信息
调用时机
下面看一个实际登录的 Controller 的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@Controller public class LoginController { //登录跳转 @RequestMapping(value = "/login", method = {RequestMethod.GET}) public String loginUI() throws Exception { return "../../login"; } //登录跳转 @RequestMapping(value = "/sxqy", method = {RequestMethod.GET}) public String loginUI2() throws Exception { return "../../login"; } //重点!!!!!! //登录表单处理 @RequestMapping(value = "/login", method = {RequestMethod.POST}) public String login(ViewEmployeeMiPsd viewEmployeeMiPsd) throws Exception { //Shiro实现登录 UsernamePasswordToken token = new UsernamePasswordToken(viewEmployeeMiPsd.getCode(), viewEmployeeMiPsd.getPsd()); Subject subject = SecurityUtils.getSubject(); //如果获取不到用户名就是登录失败,但登录失败的话,会直接抛出异常 try{ //重点!!!!!! //getAuthenticationInfo 执行时机 subject.login(token); }catch (Exception e){ e.printStackTrace(); } //重点!!!!!! //getAuthorizationInfo 执行时机 -- subject.hasRole() if (subject.hasRole("admin")) { return "redirect:/admin/showComputerProblems"; } else if (!subject.hasRole("admin")) { return "redirect:/normal/showComputerProblems"; } return "/login"; } } |
不过,getAuthorizationInfo 的执行调用方式包括上面的总共有三个:
subject.hasRole(“admin”) 或 subject.isPermitted(“admin”):自己去调用这个是否有什么角色或者是否有什么权限的时候;
@RequiresRoles(“admin”) :在方法上加注解的时候;
[@shiro.hasPermission name = “admin”][/@shiro.hasPermission]:在页面上加shiro标签的时候,即进这个页面的时候扫描到有这个标签的时候。
实现
需要注意的是,在 Shiro 实际使用中,我们是肯定会自定义一个 Realm 类的。
从上面的功能说明可以看出来,在权限控制中比较重要的验证(登录或权限)逻辑,都是在Realm中做的。Realm的类继承如下:
不同的继承,需要实现不同的方法。继承了 AuthorizingRealm 的类,都要实现上面说的 getAuthenticationInfo 和 getAuthorizationInfo 方法,来完成身份验证和权限获取。但如果自定义的 Realm 类只实现 Realm 接口的话,只需要 getAuthenticationInfo 方法就可以。下面看一个只实现 Realm 接口的自定义 Realm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class MyRealm1 implements Realm { @Override public String getName() { return "myrealm1"; } @Override public boolean supports(AuthenticationToken token) { //仅支持UsernamePasswordToken类型的Token return token instanceof UsernamePasswordToken; } @Override public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String)token.getPrincipal(); //得到用户名 String password = new String((char[])token.getCredentials()); //得到密码 if(!"zhang".equals(username)) { throw new UnknownAccountException(); //如果用户名错误 } if(!"123".equals(password)) { throw new IncorrectCredentialsException(); //如果密码错误 } //如果身份认证验证成功,返回一个AuthenticationInfo实现; return new SimpleAuthenticationInfo(username, password, getName()); } } |
但是在使用中基本上都会对账户进行权限管理,下面看一个继承 AuthorizingRealm 的自定义 Realm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
@Component public class LoginRealm extends AuthorizingRealm{ @SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同 @Resource(name = "roleServiceImpl") private RoleService roleService; @SuppressWarnings("SpringJavaAutowiringInspection")//忽略警告,下同 @Resource(name = "viewEmployeeMiPsdServiceImpl") private ViewEmployeeMiPsdService viewEmployeeMiPsdService; /** * 获取身份信息,我们可以在这个方法中,从数据库获取该用户的权限和角色信息 * 当调用权限验证时,就会调用此方法 */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String code = (String) getAvailablePrincipal(principalCollection); Role role = null; ViewEmployeeMiPsd viewEmployeeMiPsd = null; viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code); //通过用户名从数据库获取角色权限集 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); Set<String> r = new HashSet<>(); if (role != null) { String[] roles = role.getRolename().split("\\+"); for(int i = 0;i < roles.length; i++){ r.add(roles[i].toString()); } //放入该用户权限信息 info.setRoles(r); } return info; } /** * 在这个方法中,进行身份验证 * login时调用 */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //工号 String code = (String) token.getPrincipal(); //密码 String password = new String((char[])token.getCredentials()); ViewEmployeeMiPsd viewEmployeeMiPsd = null; viewEmployeeMiPsd = viewEmployeeMiPsdService.findByCode(code); if (viewEmployeeMiPsd == null) { //没有该用户 throw new UnknownAccountException(); } else if (!password.equals(viewEmployeeMiPsd.getPsd())) { //密码错误 throw new IncorrectCredentialsException(); } //身份验证通过,返回一个身份信息 AuthenticationInfo aInfo = new SimpleAuthenticationInfo(code,password,getName()); return aInfo; } } |
转自:https://blog.csdn.net/zx48822821/article/details/80503742