一、什么是Shiro?
1. 概括定义
一个强大的安全框架。执行身份认证、授权、密码学、会话管理。
2. 类似比较
Spring的安全框架,学习成本比Shiro高。但是一般简单的应用使用Shiro就够了。
3. 组成框架
Private Concerns:
Authentication:验证身份 (principal) credential (凭证)
Authorization:验证身份所具有的角色和权限
Session Management:将用户信息存储在会话中(JAVASE环境,WEB环境),以便用户下次登入时使用。
Cryptography:加密解密
Supporting Features:
Web Support Caching Concurrency Testing Run As Remember Me
二、认证流程
Subject (认证主体) .login(token)
|
Security Manager (安全管理器,分发并管理请求)
|
Authenticator (委托给认证器并调用认证策略) <-------- Authentication Strategy (策略)
|
Pluggable Realms (1 or more 源)
JDBC Realm
LDAP Realm
ActiveDirectory Realm
Custom Realm
三、使用说明
1. 添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
2. 配置文件
核心配置文件为.ini格式,可以分组的键值对形式。可以用 ;和 # 进行注释。
# define user info.
[users]
admin=123
tom=456
3. 基本用法
//用SecurityManager对指定的配置文件进行初始化
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
//绑定工具类
SecurityUtils.setSecurityManager(securityManager);
//获取主体
Subject subject = SecurityUtils.getSubject();
//登入
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123");
try{
subject.login(token);
} catch (UnkownAccountException e){
logger.error("未知用户异常", e);
} catch (IncorrectCredentialsException e){
logger.error("密码错误异常", e);
} catch (AuthenticationException e){
logger.error("认证异常", e);
}
System.out.println("是否认证:" + subject.isAuthenticated());
//退出
subject.logout(token);
System.out.println("是否认证:" + subject.isAuthenticated());
4. 默认配置
# define user info.
[users]
admin=123
tom=456
# 以下配置可以不写则相当于以下默认配置......................................................
# 默认的securitymanager
securitymanager=org.apache.shiro.mgt.defaultsecuritymanager
# 默认的认证器
authenticator=org.apache.shiro.authc.pam.modularrealmauthenticator
# 将认证器添加到SecurityManager
securitymanager.authenticators=$authenticator
# 默认的认证策略
authenticationstrategy=org.apache.shiro.authc.pam.atleastonesuccessfulstrategy
# 将认证策略添加到认证器
authenticator.authenticationstrategy=$authenticationstrategy
# 默认的Realm
iniRealm=org.apache.shiro.realm.text.inirealm
# 将realm添加到SecurityManager, 多个则以逗号隔开
securitymanager.realms=$iniRealm
四、Realm
默认有三种realm: IniRealm、JdbcRealm、PropertiesRealm
1. JdbcRealm
//用SecurityManager对指定的配置文件进行初始化
//Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//SecurityManager securityManager = factory.getInstance();
//绑定工具类
//SecurityUtils.setSecurityManager(securityManager);
//获取主体
//Subject subject = SecurityUtils.getSubject();
//上述代码抽取到ShiroUtils
Subject subject = ShiroUtils.getSubject("classpath:shiro/shiro.ini");
//登入
UsernamePasswordToken token = new UsernamePasswordToken("admin", "123");
try{
subject.login(token);
} catch (UnkownAccountException e){
logger.error("未知用户异常", e);
} catch (IncorrectCredentialsException e){
logger.error("密码错误异常", e);
} catch (AuthenticationException e){
logger.error("认证异常", e);
}
System.out.println("是否认证:" + subject.isAuthenticated());
//退出
subject.logout(token);
System.out.println("是否认证:" + subject.isAuthenticated());
#配置数据源
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
data.url=jdbc:mysql://localhost:3306/shiro
dataSource.username=root
#.ini语法 没有密码就不写,否则报错
#dataSource.password=
#使用JdbcRealm
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
#注入数据源
jdbcRealm.dataSource=$dataSource
#重写认证语句
jdbcRealm.authenticationQuery=select password from t_user where username = ?
#添加realm到SecurityManager
securityManager.realms=$jdbcRealm
解决日志显示问题:
<!-- 面向slfj的日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!-- log4j实现 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- slf4j整合log4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
2. 自定义Realm
如果使用的是QQ,微信等登入,则需要自定义Realm。
public class MyRealm extends AuthorizingRealm {
/**
* 身份认证
**/
@Override
protected AuthorizationInfo doGetAuthenticationInfo(AuthenticationToken token) throws
AuthenticationException {
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
if(password == null){
throw new UnkownAccountException("No account found for user [" + username + "]");
}
else if(!password.equals(token.getPassword())){
throw new IncorrectCredentialsException("Error password for user [" + username + "]");
}
else {
throw new AuthenticationException("Error Authentication for user [" + username + "]");
}
}
/**
* 授权
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
return null;
}
}
#使用JdbcRealm
myRealm=com.demo.shiro.MyRealm
#添加realm到SecurityManager
securityManager.realms=$myRealm
五、整合spring
1. 添加依赖
<!-- 整合web项目 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
</dependency>
<!-- 整合spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
</dependency>
2. 配置Shiro过滤器
<!-- 配置shiro过滤器,用来拦截所有请求,进行认证和授权 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.delegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
3. 定义ShiroBean
<!-- 配置密码匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="${algorithmName}"/>
<property name="hashIterations" value="${hashIterations}"/>
<property name="storedCredentialsHexEncoded" value="false"/>
</bean>
<!-- 配置ream -->
<bean id="realm" class="com.demo.shiro.JdbcSaltRealm">
<!-- 注入DataSource -->
<property name="dataSource" ref="dataSource"/>
<!-- 注入密码匹配器 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
<!-- 重写salt认证sql -->
<property name="authenticationQuery">
<value>
select password, login_name from t_user where login_name=?
</value>
</property>
</bean>
<!-- 配置SecurityManager -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realms">
<list>
<ref bean="ream"/>
</list>
</property>
</bean>
<!-- 定义一个名为shiroFilter的bean,用来配置url过滤规则 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 定义url过滤规则 -->
....
</bean
4.代码
//用于加密明文
public class PasswordServiceImpl implements PasswordService {
@Value("${algorithmName}")
private String algorithmName;
@Value("${hashIterations}")
private int hashIterations;
@Override
public String encrytPassword(Object passwordVo) throws IllegalArgumentException {
PasswordVo vo = (PasswordVo) passwordVo;
return new SimpleHash(algorithmName, vo.getPassword(), vo.getSalt(), hashIterations).toBase64();
}
@Override
public boolean passwordsMatch(Object submittedPlaintext, String encrypted) {
return false;
}
}
@RequestMapping("/login")
public String login(String loginName, String password, Model model, HttpSession session){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginName, password);
try{
subject.login(token);
User user = (User) subject.getPrincipal();
// subject.getSession() != session, but they have the same data.
session.setAttribute("user", user);
return "forward:/product/findAll";
} catch (UnkownAccountException e){
model.addAttribute("loginError", "用户名不存在");
} catch (IncorrectCredentialsException e){
model.addAttribute("loginError", "密码不正确");
} catch (AuthenticationException e){
model.addAttribute("loginError", "登入失败");
}
return "login";
}
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return
}
5. 实现登入保护
有些url的访问需要登入,如果未登入,则跳转到登入页面。
解决:配置url过滤规则
Shiro默认提供了11个过滤规则,常用的由:
1) anon(AnonymousFilter) 不需要认证
2) authc(FormAuthenticationFIlter) 需要登入认证
3) role(RolesAuthenticationFilter) 需要角色认证
4) perms(PermissionAuthorizationFilter) 需要权限认证
其他不常用:
5) authcBasic(BasicHttpAuthenticationFilter)
6) logout(LogoutFilter)
7) noSessionCreation(NoSessionCreationFilter)
8) port(PortFilter) 需要指定的端口
9) rest(HttpMethodPermissionFilter)
10) ssl(SslFilter)
11) user(UserFilter)
<!-- 定义一个名为shiroFilter的bean,用来配置url过滤规则 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- 定义url过滤规则 -->
<property name="filterChainDefinitions">
<value><!-- 由枚举类DefaultFilter提供规则 -->
/product/findAll=authc
/teacher/**=authc, roles[teacher]
/student/**=authc, roles[student]
</value>
</property>
<!-- 重写默认的登入失败页面,默认/login.jsp -->
<property name="loginUrl" value="/showLogin"/>
<!-- 重写未授权页面,默认显示401 -->
<property name="unAuthorizedUrlUrl" value="/401.jsp"/>
</bean
6. 角色和权限的验证
1) url过滤规则中配置角色
相同的url上配置多个角色需要自定义过滤器
public class CustomAuthorizationFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse, Object mappedValue)
throws Exception {
Subject subject = getSubject(request, response);
String[] roleArray = (String[]) mappedValue;
if(rolesArray == null || rolesArrays.length == 0) {
return true;
}
//判断角色
for(String role : rolesArrays) {
if(subject.hasRole(rolesArray[i])) {
return true;
}
}
return false;
}
}
<!-- 配置自定义过滤器 -->
<bean id="customRoles" class="com.demo.shiro.filter.CustomRolesAuthorizationFilter"/>
/product/findAll=authc
/teacher/delete=authc,perms[teacher:delete]
/teacher/**=authc, customRoles[teacher, manager]
/student/**=authc, roles[student]
2)重写角色sql
<!-- 配置ream -->
<bean id="realm" class="com.demo.shiro.JdbcSaltRealm">
...
<!-- 重写角色sql -->
<property name="userRolesQuery">
<value>
select r.role_name from t_user u left join
user_role ur on u.id= ur.user_id left join
t_role r on ur.role_id = r.id where u.login_name = ?
</value>
</property>
<!-- 开启权限查找 -->
<property name="permissionsLookupEnabled" value="true"/>
<!-- 重写权限sql -->
<property name="permissionsQuery">
<value>
select p.permission_name from t_role r left join
role_permission rp on r.id = rp.role_id left join
t_permission p on rp.permission_id = p.id where r.role_name = ?
</value>
</property>
</bean>
3) 重写未授权页面
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
...
<!-- 重写未授权页面,默认显示401 -->
<property name="unAuthorizedUrlUrl" value="/401.jsp"/>
</bean>
7. 自定义UserRealm
public class UserRealm extends AuthorizingRealm {
/**
* 认证
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = (String) token getPrincipal();
//mybaits query.
User user = ...;
if(user == null){
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(user,//user info
user.getPassword(),//password
ByteSource.Util.bytes(user.getSalt()),//salt
getName()//realm name
);
}
/**
* 授权
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = principals.getPrimaryPrincipal();
String username = user.getLoginName();
//mybaits query.
Set<String> roles = ..;
Set<String> permissions = ..;
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addPermissions(permissions);
return authorizationInfo;
}
}
<!-- 配置UserRealm替换默认的JdbcRealm -->
<bean id="userRealm" class="com.demo.shiro.UserRealm">
<!-- 注入密码匹配器 -->
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
8. Shiro标签
<!-- 导入标签库 -->
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<shiro:principal/> == ${sessionScope.user.loginName}
<shiro:hasAnyRoles name="teacher,manager">
...
</shiro:hasAnyRoles>
<shiro:hasAnyRoles name="student">
...
</shiro:hasAnyRoles>
<shiro:hasPermission name="teacher:delete">
...
</shiro:hasPermission>