Shiro
Shiro 是一个JAVA安全框架,apche的产品支持各种语言,专门用于权限认证、授权、用户登录、缓存、密码的轻量级框架,支持各种数据存储形式:数据库、xml文件、properties文件
Spring Security是一个重量级的框架与Shrio是同类型的产品。
Shiro内部有四大基石:
1、 身份认证。2、权限认证。3、会话管理。4、密码
一般情况下要完成权限管理,需要六张表:
1、用户表 2、用户角色中间表 3、角色表 4、角色权限中间表 5、权限表 6、菜单表
Shiro的入门案例
Login&Logout:
@Test
public void testLogin() throws Exception{
//读取shiro.ini文件,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//使用工厂创建securityManagger对象
SecurityManager securityManager = factory.getInstance();
//将securityManager设置进Shiro的上下文中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
System.out.println("是否登录:"+subject.isAuthenticated());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("root","123");
//判断是否登录,未登录就登陆一下
if (!subject.isAuthenticated()){
subject.login(usernamePasswordToken);
}
System.out.println("是否登录:"+subject.isAuthenticated());
//如果登录了就登出
if (subject.isAuthenticated()){
subject.logout();
}
System.out.println("是否登录:"+subject.isAuthenticated());
}
测试登录失败捕获异常
@Test
public void testLoginFail() throws Exception{
//读取shiro.ini文件,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//使用工厂创建securityManagger对象
SecurityManager securityManager = factory.getInstance();
//将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
System.out.println("是否登录:"+subject.isAuthenticated());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("root","123");
//判断是否登录,未登录就登陆一下
try {
if (!subject.isAuthenticated()){
subject.login(usernamePasswordToken);
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("未知用户名");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("系统异常");
}
System.out.println("是否登录:"+subject.isAuthenticated());
//如果登录了就登出
if (subject.isAuthenticated()){
subject.logout();
}
System.out.println("是否登录:"+subject.isAuthenticated());
}
授权:判断是否拥有权限
@Test
public void testAuthori() throws Exception{
//读取shiro.ini文件,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//使用工厂创建securityManagger对象
SecurityManager securityManager = factory.getInstance();
//将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
System.out.println("是否登录:"+subject.isAuthenticated());
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("root","123");
//判断是否登录,未登录就登陆一下
if (!subject.isAuthenticated()){
subject.login(usernamePasswordToken);
}
//判断当前登录用户是否拥有adimn角色
System.out.println("是否具有admin角色:"+subject.hasRole("admin"));
//判断当前登录用户是否拥有角色角色
System.out.println("是否具有guest角色:"+ subject.hasRole("guest"));
//判断当前是否拥有访问employee:delete权限
System.out.println("是否具有employee:delete权限:"+subject.isPermitted("employee:delete"));
System.out.println("是否登录:"+subject.isAuthenticated());
//如果登录了就登出
if (subject.isAuthenticated()){
subject.logout();
}
System.out.println("是否登录:"+subject.isAuthenticated());
}
自定义JpaRealm
Shiro框架中没有提供一个以SpringDataJpa来实现访问数据的 Realm,而当前项目中Dao层使用的是 SpringDataJpa,所以需要自定义一个Realm(内部采用SpringDataJpa来访问数据)
如何自定义Realm
Realm是Shiro框架中的一个接口,那么要自定义Realm的话,就只需要自定义一个 类去实现Realm接口就行了,但是,如果直接实现Realm接口的话,这个自定义的Realm只能用来做登录,无法用来做权限认 证功能。所以可以将自定义的JpaRealm类继承AuthorizingRealm类,就可以做登录和权限认证的功能了
身份认证
身份认证方法也就是用户登录,当调用subject.login()方法时,shiro就会自动调用此方法,并将Token对象传入此方法中。
此方法要求返回一个AuthtiontizingtionInfo对象,当返回的对象为null时,则表示用户名不存在,就会抛出UnknownAccountException异常。
返回的对象不为null时,Shiro会自动将传入的Token对象中的密码和查询到的密码进行比较,如果匹配,登录成功,不匹配则抛出IncorrectCredentialsException,表示密码错误
判断Employee是否为null,如果不等于null,返回Authenticationtoken接口的实现类对象传入三个参数,1、查询到的员工对象(主体对象,登录成功后Shiro会自动将此对象保存到session中),2、从数据库中查询到的密码(凭证,Shiro会将此密码与Token中的密码进行比较),3、自定义JpaRealm的名称,否则就返回空
public class JpaRealm extends AuthorizingRealm{
//登录
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将authenticationtoken类型类型强转为usernameToken,用于查询数据库
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//从Token中获取用户名
String username = usernamePasswordToken.getUsername();
//模拟查询:通过用户名查询数据库,判断是否查询到了数据
Employee employee = this.findByUsername(username);
if (employee!=null){
//设置盐值
ByteSource salt = ByteSource.Util.bytes("yinsihao");
return new SimpleAuthenticationInfo(employee,employee.getPassword(),salt,"JpaRealm");
}
return null;
}
//模拟查询
private Employee findByUsername(String username) {
if ("yinsh".equals(username)){
Employee employee = new Employee();
employee.setUsername("yinsh");
employee.setPassword("73a3c61426da086758c162b739a52fdf");
return employee;
}
return null;
}
授权
当用户登录成功后每次调用判断是否具有权限的方法时都会自动执行此方法,对当前用户进行授权,然后在用户访问需要授权才能访问的资源时,会自动查找当前用户是否拥有对应的权限
如果当前用户具有权限则放行,如果没有则被拦截,Shiro已经做好了进行拦截判断的过滤器,只需要给当前用户授权即可
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取当前登录用户
Employee employee = (Employee) principalCollection.getPrimaryPrincipal();
//创建授权对象
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//获取员工的拥有的权限
Set<String> permission = this.findPermissionByEmployeeId(employee.getId());
//进行授权,使用授权对象的设置权限方法,并传入查询到的员工权限
authorizationInfo.setStringPermissions(permission);
return authorizationInfo;
}
//模拟查询
private Set<String> findPermissionByEmployeeId(Long id) {
Set<String> permission = new HashSet<>();
permission.add("employee:index");
permission.add("employee:page");
return permission;
}
单元测试
public class _02_ShiroAuthenticationInfoTest {
//测试登录
@Test
public void test() throws Exception{
//创建securityManager对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//创建自定义JpaRealm对象
JpaRealm jpaRealm = new JpaRealm();
//将自定义JpaRealm放入securityManager中:使用自定义Realm的规则
securityManager.setRealm(jpaRealm);
//放入Shiro的上下文中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//模拟登录,获取用户名和密码
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("yinsh","123");
try {
if (!subject.isAuthenticated()){
subject.login(usernamePasswordToken);
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("系统异常");
}
System.out.println("是否登录:"+ subject.isAuthenticated());
//如果登录就退出
if (subject.isAuthenticated()){
subject.logout();
}
//再次判断是否登录
System.out.println("是否登录:"+ subject.isAuthenticated());
}
//测试授权
@Test
public void testAuthorizingthon() throws Exception{
//创建securityManager对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//创建自定义JpaRealm对象
JpaRealm jpaRealm = new JpaRealm();
//将自定义JpaRealm放入securityManager中:使用自定义Realm的规则
securityManager.setRealm(jpaRealm);
//放入Shiro的上下文中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//模拟登录,获取用户名和密码
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("yinsh","123");
try {
if (!subject.isAuthenticated()){
subject.login(usernamePasswordToken);
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("系统异常");
}
System.out.println("受否拥有employee:index权限:" + subject.isPermitted("employee:index"));
System.out.println("受否拥有employee:page权限:" + subject.isPermitted("employee:page"));
System.out.println("受否拥有employee:save权限:" + subject.isPermitted("employee:save"));
System.out.println("是否登录:"+ subject.isAuthenticated());
//如果登录就退出
if (subject.isAuthenticated()){
subject.logout();
}
//再次判断是否登录
System.out.println("是否登录:"+ subject.isAuthenticated());
}
密码加密
Shiro中有专门提供的一套方案对密码进行加密,有两个加密方式:MD5、SHA
将原密码与盐值拼接后,按照MD5方式加密,将上次加密后的密码在拼接盐值,再次加密,循环13次
@Test
public void test() throws Exception{
ByteSource salt = ByteSource.Util.bytes("yinsihao");
SimpleHash simpleHash = new SimpleHash("MD5","123",salt,13);
System.out.println(simpleHash);
}
@Test
public void test02() throws Exception{
//创建securityManager对象
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//创建自定义JpaRealm对象
JpaRealm jpaRealm = new JpaRealm();
//加密规则
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//加密方式
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
//加密次数
hashedCredentialsMatcher.setHashIterations(13);
//添加加密规则
jpaRealm.setCredentialsMatcher(hashedCredentialsMatcher);
//将自定义JpaRealm放入securityManager中:使用自定义Realm的规则
securityManager.setRealm(jpaRealm);
//放入Shiro的上下文中
SecurityUtils.setSecurityManager(securityManager);
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//模拟登录,获取用户名和密码
try {
if (!subject.isAuthenticated()){
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("yinsh","123");
subject.login(usernamePasswordToken);
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("系统异常");
}
System.out.println("是否登录:"+ subject.isAuthenticated());
//如果登录就退出
if (subject.isAuthenticated()){
subject.logout();
}
//再次判断是否登录
System.out.println("是否登录:"+ subject.isAuthenticated());
}
盐值需要在JpaRealm类中的doGetAuthenticationInfo方法返回的SimpleAuthenticationInfo的构 造方法中添加一个参数
ByteSource salt = ByteSource.Util.bytes("yinsihao");
return new SimpleAuthenticationInfo(employee,employee.getPassword(),salt,"JpaRealm");
抽取密码加密的工具类
public class MD5Utils {
//设置加密方式
private static String hashAlgorithName = "MD5";
//加密次数
private static Integer hashIterations = 13;
//盐值
private static ByteSource salt = ByteSource.Util.bytes("yinsh");
public static String getMD5Password(String source){
SimpleHash simpleHash = new SimpleHash(hashAlgorithName,source,salt,hashIterations);
return simpleHash.toString();
}
集成Spring
1、 在web.xml中添加Shiro的过滤器(Shiro的所有操作都是基于过滤器来实现的)
这是spring与shrio集成的标志,这个过滤器就表示当前项目中所有的登录、权限认证都是用Shiro框架来完成,但是这个过滤器仅仅是一个标志,不做实事
<filter>
<filter-name>shiroFilterfilter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxyfilter-class>
<init-param>
<param-name>targetFilterLifecycleparam-name>
<param-value>trueparam-value>
init-param>
filter>
<filter-mapping>
<filter-name>shiroFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
2、 Shiro的配置文件
将applicationContext.xml拷贝到项目的resources目录下,然后进行修改
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="jpaRealm" class="cn.yinsh.ibs.shiro.JpaRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"/>
<property name="hashIterations" value="13"/>
bean>
property>
bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="jpaRealm"/>
bean>
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
bean>
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<property name="loginUrl" value="/s/login"/>
<property name="successUrl" value="/s/index"/>
<property name="unauthorizedUrl" value="/s/unauthorized.jsp"/>
<property name="filterChainDefinitions">
<value>
# 不需要登录就能访问的资源
/favicon.ico = anon
/logo.png = anon
/shiro.css = anon
/s/login = anon
# 需要授权才能访问的资源
/test/test-1 = perms[employee:index]
/test/test-2 = perms[employee:page]
/test/test-3 = perms[employee:save]
# 其他所有的资源都是必须要登录后才能访问的:
/** = authc
value>
property>
bean>
beans>
spring导入shiro配置文件
<import resource="classpath:spring-shiro.xml"/>
最后进行测试
1、创建三个Jsp文件:登录界面、欢迎界面、未授权界面
2、编写登录的Controller
3、启动tomcat进行访问
启动成功后,立即自动跳转到/s/login.jsp的原因:
启动tomcat服务器会立即跳转到/s/login.jsp:是因为tomcat配置默认访问8080端口,启动成功后会立即访问localhost::8080/,由于地址没有写完整,配合tomcat的欢迎界面,地址会自动拼接成localhost::8080/index.jsp
并且index.jsp不在“不需要登录就能访问的资源”范围内,所以他是一个必须要登录才能访问的资源,而shiro检测到未登录,所以就重定向到了配置文件的loginUrl中。