DelegatingFilterProxy解析
在Spring配置文件中配置shiroFilter时,配置bean的id属性需要和DelegatingFilterProxy的<filter-name>
一致。如果不一致,控制台将打印以下错误信息。
10-Oct-2019 18:38:14.669 信息 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.ApplicationContext.log No Spring WebApplicationInitializer types detected on classpath
10-Oct-2019 18:38:14.903 信息 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.ApplicationContext.log Initializing Spring root WebApplicationContext
10-Oct-2019 18:38:19.347 严重 [RMI TCP Connection(3)-127.0.0.1] org.apache.catalina.core.StandardContext.filterStart Exception starting filter [shiroFilter]
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'shiroFilter' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:775)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1221)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:294)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1111)
at org.springframework.web.filter.DelegatingFilterProxy.initDelegate(DelegatingFilterProxy.java:337)
at org.springframework.web.filter.DelegatingFilterProxy.initFilterBean(DelegatingFilterProxy.java:242)
at org.springframework.web.filter.GenericFilterBean.init(GenericFilterBean.java:238)
at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:285)
at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:266)
at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:108)
at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4637)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5282)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:754)
at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:730)
at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:734)
at org.apache.catalina.startup.HostConfig.manageApp(HostConfig.java:1736)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:300)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:483)
at org.apache.catalina.mbeans.MBeanFactory.createStandardContext(MBeanFactory.java:432)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.tomcat.util.modeler.BaseModelMBean.invoke(BaseModelMBean.java:300)
at com.sun.jmx.interceptor.DefaultMBeanServerInterceptor.invoke(DefaultMBeanServerInterceptor.java:819)
at com.sun.jmx.mbeanserver.JmxMBeanServer.invoke(JmxMBeanServer.java:801)
at javax.management.remote.rmi.RMIConnectionImpl.doOperation(RMIConnectionImpl.java:1468)
at javax.management.remote.rmi.RMIConnectionImpl.access$300(RMIConnectionImpl.java:76)
at javax.management.remote.rmi.RMIConnectionImpl$PrivilegedOperation.run(RMIConnectionImpl.java:1309)
at javax.management.remote.rmi.RMIConnectionImpl.doPrivilegedOperation(RMIConnectionImpl.java:1401)
at javax.management.remote.rmi.RMIConnectionImpl.invoke(RMIConnectionImpl.java:829)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:357)
at sun.rmi.transport.Transport$1.run(Transport.java:200)
at sun.rmi.transport.Transport$1.run(Transport.java:197)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:573)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:834)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:688)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:687)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
抛出这个错误的原因是shiro会到IOC容器中查找<filter-name>
名字对应的filter bean。
在DelegatingFilterProxy类中包含一个initDelegate方法。
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
String targetBeanName = this.getTargetBeanName();
Assert.state(targetBeanName != null, "No target bean name set");
Filter delegate = (Filter)wac.getBean(targetBeanName, Filter.class);
if (this.isTargetFilterLifecycle()) {
delegate.init(this.getFilterConfig());
}
return delegate;
}
Debug程序查看targetBeanName的值。
DelegatingFilterProxy实际上是Filter的一个代理对象,默认情况下,Spring会到IOC容器中找和<filter-name>
对应的filter bean。也可以通过targetBeanName初始化参数来配置filter bean的id。
成功启动
filterChainDefinitions配置细节
anon:拦截器表示匿名访问(即不需要认证便可访问)
authc:拦截器表示需要身份认证通过后才可访问。
authcBasic:表示需要通过httpBasic认证。
logout:任何Session都将失效,任何身份都将失去关联
ssl:表示安全的http请求,协议为https。
url模式使用Ant风格模式
?:匹配一个字符。
*:匹配零个活多个字符串。
**:匹配路径中的零个或多个路径。
url权限采取第一次匹配优先原则
Shiro认证
步骤
1.获取当前subject,SecurityUtils.getSubject()。
2.测试当前用户是否被认证。subject的isAuthenticated()。
3.若没有认证,则将用户名和密码封装为UsernamePasswordToken对象。
4.执行登录。Subject的login()方法。
5.自定义Realm方法,从数据库中取出数据返回给Shiro。
(继承org.apache.shiro.realm包下AuthenticatingRealm类,实现其中的doGetAuthenticationInfo()方法。
6.由shiro完成密码的比对。
实现
创建表单页面
<%--
Created by IntelliJ IDEA.
User: admin
Date: 2019/10/9
Time: 21:20
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h4>Login Page</h4>
<form action="shiro/login" 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>
创建处理器
package com.fengxunxinxi.shiro.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* ShiroController class
* @author mofengyu
*
* @date 2019/10/11
*/
@Controller
@RequestMapping("/shiro")
public class ShiroController {
public String login(@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password){
/**
* 获取当前的Subject
*/
Subject subject= SecurityUtils.getSubject();
/**
* 判断当前的Subject是否认证。
*/
if(!subject.isAuthenticated()){
/**
* 如果没有认证,将用户名和密码封装成UsernamePasswordToken对象。
*/
UsernamePasswordToken usernamePasswordToken=new UsernamePasswordToken(username,password);
/**
* 设置记住我
*/
usernamePasswordToken.setRememberMe(true);
try {
/**
* 执行认证。
*/
subject.login(usernamePasswordToken);
}catch (AuthenticationException e){
System.out.println("认证失败:"+e.getMessage());
}
}
return "redirect:/list.jsp";
}
}
创建ShiroRealm
ShiroRealm继承AuthenticatingRealm抽象类,实现doGetAuthenticationInfo()方法,用于认证过程中的安全数据访问。
package com.fengxunxinxi.shiro.realm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.Realm;
/**
* ShiroRealm class
* @author mofengyu
*
* @date 2019/10/9
*/
public class ShiroRealm extends AuthenticatingRealm {
private static String userInfo="admin";
private static String info="unknown";
private static String exception="monster";
private static String password="123456";
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/**
* 把AuthenticationToken转换为UsernamePasswordToken
*/
UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)authenticationToken;
/**
* 从UsernamePasswordToken中获取username
*/
String username=usernamePasswordToken.getUsername();
/**
* 查询数据库,查询该用户名的相关信息。
*/
/**
* 若用户不存在。抛出UnknownAccountException异常。
*/
if(info.equals(username)){
throw new UnknownAccountException("用户不存在");
}
/**
* 根据获取的用户信息决定是否抛出AuthenticationException
*/
if(exception.equals(username)){
throw new AuthenticationException("用户被锁定");
}
/**
* 根据用户信息来构建AuthenticationInfo对象并返回。
* principal认证的实体信息,可以是username,也可以是对应的用户实体类对象。
*/
Object principal=userInfo;
/**
* credentials,密码
*/
Object credentials=password;
/**
* realmName当前realm对象的name,调用父类的getName()方法。
*/
String realmName=getName();
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(principal,credentials,realmName);
return authenticationInfo;
}
}
测试认证
添加清除认证信息
<%--
Created by IntelliJ IDEA.
User: admin
Date: 2019/10/9
Time: 21:20
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
<h4>list Page</h4>
<a href="shiro/logout">Logout</a>
</body>
</html>
<property name="filterChainDefinitions">
<value>
/login.jsp=anon
/shiro/login=anon
/shiro/logout=logout
/** =authc
</value>
</property>
Shiro密码比对
Debug运行测试程序
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.apache.shiro.authc.credential;
import java.security.MessageDigest;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.codec.CodecSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
private static final Logger log = LoggerFactory.getLogger(SimpleCredentialsMatcher.class);
public SimpleCredentialsMatcher() {
}
protected Object getCredentials(AuthenticationToken token) {
return token.getCredentials();
}
protected Object getCredentials(AuthenticationInfo info) {
return info.getCredentials();
}
protected boolean equals(Object tokenCredentials, Object accountCredentials) {
if (log.isDebugEnabled()) {
log.debug("Performing credentials equality check for tokenCredentials of type [" + tokenCredentials.getClass().getName() + " and accountCredentials of type [" + accountCredentials.getClass().getName() + "]");
}
if (this.isByteSource(tokenCredentials) && this.isByteSource(accountCredentials)) {
if (log.isDebugEnabled()) {
log.debug("Both credentials arguments can be easily converted to byte arrays. Performing array equals comparison");
}
byte[] tokenBytes = this.toBytes(tokenCredentials);
byte[] accountBytes = this.toBytes(accountCredentials);
return MessageDigest.isEqual(tokenBytes, accountBytes);
} else {
return accountCredentials.equals(tokenCredentials);
}
}
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
}
SimpleCredentialsMatcher类中含有一个doCredentialsMatch()方法。
可以看到从表单输入的数据和从数据库得到的数据。点击调试的上一步。
程序跳转到了AuthenticatingRealm类的assertCredentialsMatch()方法。在这个方法中通过CredentialsMatcher来实现密码比对。
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.");
}
}
密码加密
对处理器中获取的密码加密需配置相关的credentialsMatcher。
<bean id="jdbcRealm" class="com.fengxunxinxi.shiro.realm.ShiroRealm">
<property name="credentialsMatcher">
<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<property name="hashAlgorithmName" value="MD5"></property>
<property name="hashIterations" value="1024"></property>
</bean>
</property>
</bean>
保存加密的数据到数据库中可以通过SimpleHash()。
public static void main(String[] args) {
String hashAlgorithmName="MD5";
Object credentials="123456";
Object salt=null;
int hashIterations=1024;
Object result=new SimpleHash(hashAlgorithmName,credentials,salt,hashIterations);
System.out.println(result);
}
盐值加密
对于相同的密码可以通过盐值加密成不同的字符串。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
/**
* 把AuthenticationToken转换为UsernamePasswordToken
*/
UsernamePasswordToken usernamePasswordToken=(UsernamePasswordToken)authenticationToken;
/**
* 从UsernamePasswordToken中获取username
*/
String username=usernamePasswordToken.getUsername();
/**
* 查询数据库,查询该用户名的相关信息。
*/
/**
* 若用户不存在。抛出UnknownAccountException异常。
*/
if(info.equals(username)){
throw new UnknownAccountException("用户不存在");
}
/**
* 根据获取的用户信息决定是否抛出AuthenticationException
*/
if(exception.equals(username)){
throw new AuthenticationException("用户被锁定");
}
/**
* 根据用户信息来构建AuthenticationInfo对象并返回。
* principal认证的实体信息,可以是username,也可以是对应的用户实体类对象。
*/
Object principal=userInfo;
/**
* credentials,密码
*/
Object credentials=password;
/**
* realmName当前realm对象的name,调用父类的getName()方法。
*/
String realmName=getName();
/**
* 加密的盐值。
*/
ByteSource credentialsSalt=ByteSource.Util.bytes(userInfo);
SimpleAuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(principal,credentials,credentialsSalt,realmName);
return authenticationInfo;
}
对于doGetAuthenticationInf()方法返回的AuthenticationInfo对象需要带上盐值,盐值是一个ByteSource对象。
使用用户名作为盐值加密。
public static void main(String[] args) {
String hashAlgorithmName="MD5";
Object credentials="123456";
Object salt= ByteSource.Util.bytes("admin");
int hashIterations=1024;
Object result=new SimpleHash(hashAlgorithmName,credentials,salt,hashIterations);
System.out.println(result);
}
使用MD5盐值加密后的密码。