赞
踩
分类:shiro(Apache) 和 springsecurity
定义:属于系统安全的范畴,实现对用户访问系统的控制,按照安全规则控制用户可以访问且只能访问自己被授权的资源。
包括:身份认证(Authenticator) 和 授权(Authorizer)
shiro官网:https://shiro.apache.org/
shiro的核心架构图 重要部分:security manager(安全管理器)其中包括:
定义:判断一个用户是否为合法用户的过程,其实就是我们登陆的过程。
关键对象:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.8.0</version>
</dependency>
[users]
wyh = 1126
wqj = 0602
public class testAuthenticator { public static void main(String[] args) { //1.新建一个安全管理器 DefaultSecurityManager securityManager = new DefaultSecurityManager(); //2.设置realm,获得要验证的值 securityManager.setRealm(new IniRealm("classpath:shiro.ini")); // 3.将安全管理器加载进工具类 SecurityUtils.setSecurityManager(securityManager); // 4.通过工具类获得对象 Subject subject = SecurityUtils.getSubject(); // 5.设置token:token相当于我们输入的用户名和密码 UsernamePasswordToken wyh = new UsernamePasswordToken("we", "1126"); try { subject.login(wyh); System.out.println(subject.isAuthenticated()); }catch (IncorrectCredentialsException e){ System.out.println("密码错误!"); }catch (UnknownAccountException e){ System.out.println("用户名错误"); } } }
主要执行身份验证的是subject.login这个方法
这个方法调用了安全管理器的login方法:
Subject subject = this.securityManager.login(this, token);
调用的是defaultSecurityManager中的login方法:
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException {
AuthenticationInfo info;
try {
info = this.authenticate(token);
这里调用了AuthenticatingSecurityManager的authenticate方法:
private Authenticator authenticator = new ModularRealmAuthenticator();
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
return this.authenticator.authenticate(token);
}
调用了AbstractAuthenticator的authenticate方法:
在这里判断了传入的token是否为空,并创建了一个info值,为info值赋值。
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
if (token == null) {
throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
} else {
log.trace("Authentication attempt received for token [{}]", token);
AuthenticationInfo info;
try {
info = this.doAuthenticate(token);
if (info == null) {
String msg = "No account information found for authentication token [" + token + "] by this Authenticator instance. Please check that it is configured correctly.";
throw new AuthenticationException(msg);
}
执行的是ModularRealmAuthenticator的 doAuthenticate方法:
第一个assertRealmsConfigured方法是对realm值做了一个判断,是否有realm值,如果没有则报错。
然后对realm的个数进行判断,是否只有一个realm,我们实验中是只有一个realm的,所以执行本类下的doSingleRealmAuthentication方法。
执行getAuthenticationInfo方法,获取info的值
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { this.assertRealmsConfigured(); Collection<Realm> realms = this.getRealms(); return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken); } protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } else { AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } else { return info; } } }
调用的是AuthenticatingRealm的getAuthenticationInfo方法。
因为没有将info存入缓存,所以执行doGetAuthenticationInfo方法获得info。
然后进行判断info是否为空,执行本类中的assertCredentialsMatch方法进行token和info中的密码是否一致。
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = this.getCachedAuthenticationInfo(token); if (info == null) { info = this.doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { this.cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { this.assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
assertCredentialsMatch方法是判断密码是否匹配的方法
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException {
CredentialsMatcher cm = this.getCredentialsMatcher();
if (cm != null) {
if (!cm.doCredentialsMatch(token, info)) {
String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials.";
throw new IncorrectCredentialsException(msg);
}
} else {
throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify credentials during authentication. If you do not wish for credentials to be examined, you can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance.");
}
}
执行SimpleAccountRealm类的doGetAuthenticationInfo方法:
通过UsernamePasswordToken类中的getUsername方法获得用户名username,再通过getUser方法通过用户名在realm中获取整个user对象。这一步完成了对username的判断。
然后判断其是否有锁,密码是否失效过期。将account返回成info。(account里存放了依据用户名获得的用户对象)
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken)token;
SimpleAccount account = this.getUser(upToken.getUsername());
if (account != null) {
if (account.isLocked()) {
throw new LockedAccountException("Account [" + account + "] is locked.");
}
if (account.isCredentialsExpired()) {
String msg = "The credentials for account [" + account + "] are expired";
throw new ExpiredCredentialsException(msg);
}
}
return account;
}
SimpleAccount中存放了简单的用户信息:
public class SimpleAccount implements Account, MergableAuthenticationInfo, SaltedAuthenticationInfo, Serializable {
private SimpleAuthenticationInfo authcInfo;
private SimpleAuthorizationInfo authzInfo;
private boolean locked;
private boolean credentialsExpired;
public SimpleAccount(Object principal, Object credentials, String realmName) {
this((PrincipalCollection)(principal instanceof PrincipalCollection ? (PrincipalCollection)principal : new SimplePrincipalCollection(principal, realmName)), credentials);
}
总结:
用户名的校验:在SimpleAccountRealm类的doGetAuthenticationInfo方法中进行,并在这个方法中获得了对应用户名的用户对象accent。
密码的校验:AuthenticatingRealm的assertCredentialsMatch方法。是自动的不需要我们操作。
注:
里面的token存放的用户输入的值,info中存放的是realm中取到的值。
结构关系:
所以我们要连接数据库时,重写一个类继承AuthorizingRealm并覆盖相应方法,将用户名与制定数值进行校验。
我们自己开发的过程中是需要连接数据库的,我们需要自定义一个realm,值为数据库内的数据。
从上面的源码我们知道,授权和认证的具体方法是在doGetAuthorizationInfo和doGetAuthenticationInfo两个方法中实现的,所以我们需要的就是重写这两个方法实现目的。
public class custormerRealm extends AuthorizingRealm{ // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 1.先获取用户名 String principal = (String) authenticationToken.getPrincipal(); // 2.进行用户名验证 if("wyh".equals(principal)){ SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal,"1234",this.getName()); return authenticationInfo; } return null; } }
代码中模拟了从数据库中拿取数据。
注:密码的校验是自动完成的,不需要我们自己写,但是我们日常的使用中会遇到密码加密的情况,这时就需要对shiro提供的密码校验方法进行一个改写。
注:都是在service层中进行!
代码演示:
public class TestShiroMD5 {
public static void main(String[] args) {
// 使用MD5加密
Md5Hash md5Hash = new Md5Hash("1234");
System.out.println(md5Hash);
// 使用MD5 + salt
Md5Hash md5Hash1 = new Md5Hash("1234","X0#j2");
System.out.println(md5Hash1);
// 使用MD5 + salt + hash散列
Md5Hash md5Hash2 = new Md5Hash("1234","X0#j2",1024);
System.out.println(md5Hash2);
}
}
结果:可以看出,相同的密码经过不同的加密方法后得到的结果是不同的。
shiro中的实现:
public class CustomerMd5Realm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String principal = (String) authenticationToken.getPrincipal(); if (principal.equals("wyh")){ return new SimpleAuthenticationInfo(principal, //数据库中经过加密后的密码 "955452dbdeeb30414b372ceec8805d49", //如果要加盐需要将盐的值标明 ByteSource.Util.bytes("X0#j2"), this.getName()); } return null; } }
public class TestCustomerMd5Realm { public static void main(String[] args) { //SecurityManager管理着认证、授权、会话管理,是shiro的核心 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //为我们的SecurityManager设置一个域,域中需要包含的是需要对比的数据 CustomerMd5Realm realm = new CustomerMd5Realm(); //为realm设置密码匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); hashedCredentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(hashedCredentialsMatcher); defaultSecurityManager.setRealm(realm); //将安全管理器注入安全工具 SecurityUtils.setSecurityManager(defaultSecurityManager); //获取subject,可以理解为一个用户,将一个用户抽象为一个Subject对象 Subject subject = SecurityUtils.getSubject(); //设置token用户信息,这里相当于我们从页面获取的数据 UsernamePasswordToken wyh = new UsernamePasswordToken("wyh", "1234"); try { subject.login(wyh); System.out.println("登录成功"); } catch (UnknownAccountException e) { e.printStackTrace(); System.out.println("用户名错误"); } catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误"); } } }
核心是在 realm.setCredentialsMatcher,realm中使用默认的SimpleCredentialsMatcher进行密码的验证,其中验证的方法就是通过 equals 方法进行值的比较,因为用户输入的一定是明文密码,而数据库中存储的是加密后的密码,所以使用默认的 CredentialsMatcher匹配 肯定会有问题,我们需要重给realm一个CredentialsMatcher。
realm提供了setCredentialsMatcher()方法,我们可以自己设置 CredentialsMatcher。
只要我们在 realm 中设置了盐值后,会自动给我们的密码加盐。
核心代码:
CustomerMd5Realm realm = new CustomerMd5Realm();
// 为realm设置密码匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密的类型为md5
hashedCredentialsMatcher.setHashAlgorithmName("md5");
// 设置散列值
hashedCredentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(hashedCredentialsMatcher);
授权,即访问控制,控制谁能访问哪些资源。一定是基于认证的基础之上发生的。
关键对象:
who 对 what 进行 how 操作。
who:主体即subject,主体是需要访问系统中资源的对象。
what:即Resource资源
how:权限,规定了主体对资源的操作许可,权限离开资源没有意义。
授权方式:
if(subject.hasRole("admin")){
//操作什么资源
}
if(subject.isPermission("user:update:01")){//资源实例:比如控制这个subject拥有修改序号为01的用户的权限
//对01用户进行修改
}
if(subject.isPermission("user:update:*")){//资源类型:控制这个subject拥有修改方法的权限
//对用户进行修改
}
权限字符串:
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的哪个实例具有什么操作。(* 代表所有,是最大的权限)
核心:给用户添加角色如 admin、user等
@Override
//参数是当前subject的身份集合
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获得的是主身份
String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal();
System.out.println("身份信息: "+primaryPrincipal);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRole("admin");
simpleAuthorizationInfo.addRole("user");
return simpleAuthorizationInfo;
}
if (subject.isAuthenticated()){
//基于单角色的角色控制
System.out.println(subject.hasRole("user"));
//基于多角色的角色控制
System.out.println(subject.hasAllRoles(Arrays.asList("user","admin","super")));
//是否具有其中一个角色
boolean[] booleans = subject.hasRoles(Arrays.asList("user", "admin", "super"));
for (boolean aBoolean : booleans) {
System.out.println(aBoolean);
}
}
结果展示:
可以发现,每调用一次 hasRole()就会调用一次 realm 中的 doGetAuthorizationInfo()方法。(其他两种方法的底层就是实现了 hasRole 方法)
//授权 @Override //参数是当前subject的身份集合 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { //获得的是主身份 String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println("身份信息: "+primaryPrincipal); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole("admin"); simpleAuthorizationInfo.addRole("user"); simpleAuthorizationInfo.addStringPermission("user:*:*"); simpleAuthorizationInfo.addStringPermission("food:delete"); return simpleAuthorizationInfo; }
if (subject.isAuthenticated()){
System.out.println(subject.isPermitted("user:delete:*"));
boolean[] permitted = subject.isPermitted("user:*:01", "food:update:*");
for (boolean b : permitted) {
System.out.println(b);
}
}
思路:
首先shiro需要拦截所有的请求,并对每一个请求进行判断是否认证通过,再对认证通过的请求进行授权处理。
使用 ShiroFilter 进行拦截操作。在 ShiroFilter 中使用SecurityManger 进行认证和授权操作。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。