1.1.身份验证
身份验证:一般需要提供如身份ID等一些标识信息来表明登录者的身份,如提供email,用户名/密码来证明。
在shiro中,用户需要提供principals(身份)和credentials(证明)给shiro,从而应用能验证用户身份。
principals:身份,即主体的标识属性,可以是任何属性,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/邮箱/手机号。
credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
最常见的principals和credentials组合就是用户名/密码。
1.2.shiro认证思路分析(1)获取当前的Subject,调用SecurityUtils.getSubject();
(2)测试当前的用户是否已经被认证,即是否已经登录。调用Subject的isAuthenticated();
(3)若没有被认证,则把用户名和密码封装为UsernamePasswordToken对象。
①创建一个表单页面
②把请求提交到SpringMVC的Handler
③获取用户名和密码
(4)执行登录:调用Subject的login(AuthenticationToken)方法。
(5)自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro。
①实际上需要继承org.apache.shiro.realm.AuthenticatingRealm类
②实现doGetAuthenticationInfo(AuthenticationToken)方法
(6)由shiro完成对密码的比对。
1.3.实现认证的相应代码
这部分包含了MD5加密、MD5盐值加密、多realms、认证策略
(1)编写相应的login.jsp、list.jsp页面。
(2)controller
@Controller
@RequestMapping("/shiro")
public class ShiroController {
@RequestMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password){
Subject currentUser = SecurityUtils.getSubject();
if (!currentUser.isAuthenticated()) {
// 把用户名和密码封装为 UsernamePasswordToken对象
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
token.setRememberMe(true);
try {
currentUser.login(token);
}
catch (AuthenticationException ae) {
System.out.println("登录失败: " + ae.getMessage());
}
}
return "redirect:/list.jsp";
}
}
(3)第一个realm,ShiroRealm.java,密码采用MD5盐值加密。盐值需要唯一:一般使用随机字符串或 user id。
public class ShiroRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[FirstRealm] doGetAuthenticationInfo");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
if("monster".equals(username)){
throw new LockedAccountException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
}
第二个realm。SecondRealm.java,密码采用SHA1盐值加密。
public class SecondRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[SecondReaml] doGetAuthenticationInfo");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
if("monster".equals(username)){
throw new LockedAccountException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
if("admin".equals(username)){
credentials = "ce2f6417c7e1d32c1d81a797ee0b499f87c5de06";
}else if("user".equals(username)){
credentials = "073d4c3ae812935f23cb3f2a71943f49e082a718";
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo("secondRealmName", credentials, credentialsSalt, realmName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "SHA1";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("admin");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
}
(4)shiro的相应配置。
注意多relam的配置和认证策略。
/login.jsp = anon /shiro/login = anon /shiro/logout = logout /** = authc
结果展示:
认证策略(AuthenticationStrategy):如果多个realm,怎样才能认证成功。
AuthenticationStrategy接口的默认实现:
•FirstSuccessfulStrategy:只要有一个Realm验证成功即可,只返回第一个Realm身份验证成功的认证信息,其他的忽略;
•AtLeastOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将返回所有Realm身份验证成功的认证信息;
•AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份验证成功的认证信息,如果有一个失败就失败了。
ModularRealmAuthenticator默认是AtLeastOneSuccessfulStrategy策略。
2.shiro授权
授权,也叫访问控制,即在应用中控制谁访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
主体(Subject):访问应用的用户,在Shiro中使用Subject代表该用户。用户只有授权后才允许访问相应的资源。
资源(Resource):在应用中用户可以访问的URL,比如访问JSP页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。
权限(Permission):安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如:访问用户列表页面查看/新增/修改/删除用户数据(即很多时候都是CRUD式权限控制)等。权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许。Shiro支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)。
角色(Role):权限的集合,一般情况下会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。
2.1.shiro授权方式
Shiro支持三种方式的授权:
编程式:通过写if/else授权代码块完成。
注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出相应的异常。
JSP/GSP标签:在JSP/GSP页面通过相应的标签完成。
2.2.授权的实现
授权需要继承AuthorizingRealm类,并实现其doGetAuthorizationInfo方法。
AuthorizingRealm类继承自AuthenticatingRealm,但没有实现AuthenticatingRealm中的doGetAuthenticationInfo,所以认证和授权只需要继承AuthorizingRealm就可以了,同时实现他的两个抽象方法。
(1)编写相应的页面list.jsp和群演页面admin.jsp、user.jsp、unauthorized.jsp。
list.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
Title
List Page
Welcome:
Admin Page
User Page
Logout
(2)修改ShiroRealm.java为
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
System.out.println("[FirstRealm] doGetAuthenticationInfo");
//1. 把 AuthenticationToken 转换为 UsernamePasswordToken
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
//2. 从 UsernamePasswordToken 中来获取 username
String username = upToken.getUsername();
//3. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
System.out.println("从数据库中获取 username: " + username + " 所对应的用户信息.");
//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
if("unknown".equals(username)){
throw new UnknownAccountException("用户不存在!");
}
//5. 根据用户信息的情况, 决定是否需要抛出其他的 AuthenticationException 异常.
if("monster".equals(username)){
throw new LockedAccountException("用户被锁定");
}
//6. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 通常使用的实现类为: SimpleAuthenticationInfo
//以下信息是从数据库中获取的.
//1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象.
Object principal = username;
//2). credentials: 密码.
Object credentials = null; //"fc1709d0a95a6be30bc5926fdb7f22f4";
if("admin".equals(username)){
credentials = "038bdaf98f2037b31f1e75b5b4c9b26e";
}else if("user".equals(username)){
credentials = "098d2c478e9c11555ce2823231e02ec1";
}
//3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
String realmName = getName();
//4). 盐值.
ByteSource credentialsSalt = ByteSource.Util.bytes(username);
SimpleAuthenticationInfo info = null; //new SimpleAuthenticationInfo(principal, credentials, realmName);
info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
return info;
}
public static void main(String[] args) {
String hashAlgorithmName = "MD5";
Object credentials = "123456";
Object salt = ByteSource.Util.bytes("user");;
int hashIterations = 1024;
Object result = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations);
System.out.println(result);
}
//授权会被 shiro 回调的方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
//1. 从 PrincipalCollection 中来获取登录用户的信息
Object principal = principals.getPrimaryPrincipal();
//2. 利用登录的用户的信息来用户当前用户的角色或权限(可能需要查询数据库)
Set roles = new HashSet<>();
roles.add("user");
if("admin".equals(principal)){
roles.add("admin");
}
//3. 创建 SimpleAuthorizationInfo, 并设置其 reles 属性.
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//4. 返回 SimpleAuthorizationInfo 对象.
return info;
}
}
(3)shiro配置所做的修改:
/login.jsp = anon /shiro/login = anon /shiro/logout = logout /user.jsp = roles[user] /admin.jsp = roles[admin] # everything else requires authentication: /** = authc
结果展示:当用户为admin时和当页面为user时页面的展示是不同的。
优化:对于我们的shirofilter,在实际开发中,我们一般都是这样的从数据库表中初始化资源和权限,所以我们采用以下的方式。
public class FilterChainDefinitionMapBuilder {
public linkedHashMap buildFilterChainDefinitionMap(){
linkedHashMap map = new linkedHashMap<>();
map.put("/login.jsp", "anon");
map.put("/shiro/login", "anon");
map.put("/shiro/logout", "logout");
map.put("/user.jsp", "authc,roles[user]");
map.put("/admin.jsp", "authc,roles[admin]");
map.put("/list.jsp", "user");
map.put("/**", "authc");
return map;
}
}
对于shiro标签有:guest标签、user标签、authenticated标签、notAuthenticated标签、pincipal标签、hasRole标签、hasAnyRoles标签。
参考:尚硅谷



