身份验证
一般需要提供如身份 ID 等一下标识信息来表名登录者的身份,如提供email,用户名/密码来证明。
在shiro中,用户需要提供 principals(身份)和 credentials(证明)给shiro,从而应用能够验证用户身份。
- principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个 Primary principals,一般是用户名/邮箱/手机号;
- credentials 凭证,即只有主体知道的安全值,如密码等;
最常见的 principals 和 credentials 组合就是 用户名/密码了。
基本流程
- 获取当前的Subject,调用SecurityUtils.getSubject();
- 测试当前用户是否已经被认证.即是否已经登录,调用Subject#isAuthencticated()方法;
- 若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象;
- 执行登录,调用Subject#login(AuthenticationToken token)方法,其会自动委托给SecurityManager;
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- 自定义Realm方法,从数据库中获取对应的记录,返回给Shiro;
-a.实际上需要继承 org.apache.shiro.realm.AuthenticatingRealm 类;
-b.实现 doGetAuthenticationInfo(AuthenticationToken token) 方法. - 由shiro完成对密码的比对。
实现认证流程
- 将ShiroRealm修改为以下代码:
public class ShiroRealm extends AuthenticatingRealm {
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("2. " + token.hashCode());
return null;
}
}
- 实现请求Controller编码
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@RequestMapping("/shiroLogin")
public String login(@RequestParam("username") String username,@RequestParam("password") String password){
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
token.setRememberMe(true);
try {
System.out.println("1. " + token.hashCode());
subject.login(token);
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
return "redirect:/list.jsp";
}
}
- login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>login</title>
</head>
<body>
<h4>login page</h4>
<form action="shiro/shiroLogin" method="POST">
username:<input type="text" name="username"/><br><br>
password:<input type="password" name="password"/><br><br>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
4.filterChainDefinitions配置
<property name="filterChainDefinitions">
<value>
/login.jsp = anon
/shiro/shiroLogin = anon
/** = authc
</value>
</property>
- 运行项目
在登录页面随意输入用户名和密码,登录失败,两次打印的token#hashCode()相等,后台出现"org.apache.shiro.authc.UnknownAccountException"异常,这是因为没有具体实现认证过程,接下来将完成认证的Realm。
实现认证Realm
例子:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken转换为UserNamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.从UserNamePasswordToken中获取username
String username = upToken.getUsername();
//3.调用dao层方法,从数据库中查询username对应的用户记录
System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
//4.若用户不存在,则可以抛出 UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5.根据用户信息的情况,觉得是否需要抛出其他的异常.
if ("monster".equals(username)) {
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
//principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
Object principal = username;
//credentials:密码
Object credentials = "123456";
//realmName:当前realm对象的name.调用父类的getName()
String realmName = getName();
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(principal, credentials, realmName);
return info;
}
启动项目:
- 在登录页面输入"unknown"作为用户名时,抛出用户不存在异常;
- 输入"monster"作为用户名时,抛出用户被锁定异常;
- 输入其他用户名+密码非"123456"时,认证不通过;
- 输入其他用户名+密码"123456"时,认证通过,重定向到list.jsp页面;
密码比对
通过 AuthenticatingRealm 的 credentialsMatcher 属性来进行的密码的比对!
- 如何把一个字符串加密为MD5
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object source = "123456";
Object salt = null;
int hashIterations = 3;
SimpleHash hash = new SimpleHash(hashAlgorithmName, source, salt, hashIterations);
System.out.println(hash.toString());
}
- 替换当前 Realm 的 credentialsMatcher 属性,直接使用 HashedCredentialsMatcher 对象,并设置加密算法即可。
<bean id="shiroRealm" class="org.keyhua.shiro.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密算法-->
<property name="hashAlgorithmName" value="MD5"></property>
<!--加密次数-->
<property name="hashIterations" value="3"></property>
</bean>
</property>
</bean>
- MD5盐值加密
- 认证时返回 SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName) 构造器
- 使用 ByteSource.Util.bytes() 来计算盐值
- 盐值需要唯一:一般使用随机字符串或用户id
- 使用 new SimpleHash(hashAlgorithmName, source, salt, hashIterations) 来计算盐值加密后的密码
修改认证实现:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.把AuthenticationToken转换为UserNamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.从UserNamePasswordToken中获取username
String username = upToken.getUsername();
//3.调用dao层方法,从数据库中查询username对应的用户记录
System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
//4.若用户不存在,则可以抛出 UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5.根据用户信息的情况,觉得是否需要抛出其他的异常.
if ("monster".equals(username)) {
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
//principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
Object principal = username;
//credentials:密码
Object credentials = null;
if ("admin".equals(username)){
credentials = "9aa75c4d70930277f59d117ce19188b0";
} else if ("user".equals(username)) {
credentials = "dd957e81b004227af3e0aa4bde869b25";
}
//realmName:当前realm对象的name.调用父类的getName()
String realmName = getName();
//盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null;
//info = new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
启动项目,用户名 user/admin,密码123456,认证通过。
多Realm验证
再自定义一个relam:
public class SecondRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("[SecondRealm] doGetAuthenticationInfo");
//1.把AuthenticationToken转换为UserNamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2.从UserNamePasswordToken中获取username
String username = upToken.getUsername();
//3.调用dao层方法,从数据库中查询username对应的用户记录
System.out.println("从数据库中获取username:" + username + " 所对应的用户信息。");
//4.若用户不存在,则可以抛出 UnknownAccountException 异常
if ("unknown".equals(username)) {
throw new UnknownAccountException("用户不存在");
}
//5.根据用户信息的情况,觉得是否需要抛出其他的异常.
if ("monster".equals(username)) {
throw new LockedAccountException("用户被锁定");
}
//6.根据用户的情况,来构造 AuthenticationInfo 对象并返回
//principal认证实体,可以是username,也可以是数据表对应的用户的实体类的对象
Object principal = username;
//credentials:密码
Object credentials = null;
if ("admin".equals(username)){
credentials = "28078bcf86c16b80329aa523afb74da57ffb8a11";
} else if ("user".equals(username)) {
credentials = "393c16607f34db540e1ec19ab2829044e98efaca";
}
//realmName:当前realm对象的name.调用父类的getName()
String realmName = getName();
//盐值
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null;
//info = new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
}
修改applicationContext.xml配置文件:
<!-- 1.配置SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="cacheManager" ref="cacheManager" />
<property name="authenticator" ref="authenticator" />
<property name="realms">
<list>
<ref bean="shiroRealm" />
<ref bean="secondRealm"/>
</list>
</property>
</bean>
<!--2.配置CacheManager
2.1.需要引入ehcache的jar及配置文件
-->
<bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
</bean>
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
</bean>
<!--3.配置realm
3.1 直接实现了Realm接口的bean
-->
<bean id="shiroRealm" class="org.keyhua.shiro.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密算法-->
<property name="hashAlgorithmName" value="MD5"></property>
<!--加密次数-->
<property name="hashIterations" value="3"></property>
</bean>
</property>
</bean>
<bean id="secondRealm" class="org.keyhua.shiro.SecondRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!--加密算法-->
<property name="hashAlgorithmName" value="SHA1"></property>
<!--加密次数-->
<property name="hashIterations" value="3"></property>
</bean>
</property>
</bean>
多个Realm进行验证时,验证规则通过 AuthenticationStrategy 接口指定
AuthenticationStrategy接口的默认实现:
-FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
-AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息;
-AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了;
- ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy 策略
认证策略的设置:
<bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
<!-- 设置认证策略 -->
<property name="authenticationStrategy">
<bean class="org.apache.shiro.authc.pam.AllSuccessfulStrategy"></bean>
</property>
<property name="realms">
<list>
<ref bean="shiroRealm" />
<ref bean="secondRealm"/>
</list>
</property>
</bean>