SpringSecurity(1)
其实啊,这部分我是最不想写的,因为最麻烦的也是这部分,真的是非常非常的麻烦。关于SpringSecurity的配置,让我折腾了好半天,网上的配置方式一大把,但总有一些功能不完全,版本不是最新等等的问题在,所以几乎没有一个教程,是可以整个贯通的。当然我的意思不是说那些不好,那些也不错,但就对于我来说,还不够全面。另外,SpringSecurity的替代品是shiro,据说,两者的区别在于,前者涵盖的范围更广,但前者也相对学习成本更高。又因为SpringSecurity是Spring家族的成员之一,所以在Spring框架下应用的话,可以做到非常高度的自定义,算是非常灵活的安全框架,就是配置起来,真心复杂。
SpringSecurity的配置文件
目录:resource/config/spring,文件名:applicationContext-security.xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="http://www.springframework.org/schema/security"
xmlns:aop="http://www.springframework.org/schema/aop"
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-4.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"> <!--过滤资源 start-->
<!--不进行拦截的静态资源-->
<sec:http pattern="/css/*" security="none"/>
<sec:http pattern="/images/*" security="none"/>
<sec:http pattern="/images/**" security="none"/>
<sec:http pattern="/js/*" security="none"/>
<sec:http pattern="/fonts/*" security="none"/>
<!--不进行拦截的页面-->
<sec:http pattern="/WEB-INF/views/index.jsp" security="none"/>
<!--<sec:http pattern="WEB-INF/views/login.jsp" security="none"/>-->
<!--过滤资源 end--> <!--权限配置及自定义登录界面 start-->
<sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager">
<sec:form-login
login-page="/user/login"
login-processing-url="/login.do"
authentication-success-handler-ref="loginController"
authentication-failure-handler-ref="loginController"/>
<!--登出-->
<sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/>
<!--session管理及单点登录-->
<sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/>
<!--资源拦截器配置-->
<sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
<sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/>
</sec:http> <!--自定义验证结果控制器-->
<bean id="loginController" class="com.magic.rent.controller.LoginAuthenticationController">
<property name="successURL" value="/user/home"/>
<property name="failURL" value="/user/login"/>
<property name="attrName" value="loginResult"/>
<property name="byForward" value="false"/>
<property name="userInfo" value="userInfo"/>
</bean> <sec:authentication-manager alias="myAuthenticationManager">
<sec:authentication-provider ref="daoAuthenticationProvider"/>
</sec:authentication-manager> <!--权限查询服务-->
<bean id="cachingUserDetailsService"
class="org.springframework.security.config.authentication.CachingUserDetailsService">
<constructor-arg name="delegate" ref="webUserDetailsService"/>
<property name="userCache">
<bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache">
<property name="cache" ref="userEhCacheFactory"/>
</bean>
</property>
</bean> <bean id="daoAuthenticationProvider"
class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="messageSource" ref="messageSource"/>
<property name="passwordEncoder" ref="messageDigestPasswordEncoder"/>
<property name="userDetailsService" ref="cachingUserDetailsService"/>
<property name="saltSource" ref="saltSource"/>
<property name="hideUserNotFoundExceptions" value="false"/>
</bean> <!--MD5加密盐值-->
<bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource">
<property name="userPropertyToUse" value="username"/>
</bean> <!--决策管理器 start-->
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg name="decisionVoters">
<list>
<ref bean="roleVoter"/>
<ref bean="authenticatedVoter"/>
</list>
</constructor-arg>
<property name="messageSource" ref="messageSource"/>
</bean>
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter">
<property name="rolePrefix" value="ROLE_"/>
</bean>
<bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter"/>
<!--决策管理器 end--> <!--资源拦截器 start-->
<bean id="filterSecurityInterceptor"
class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="authenticationManager" ref="myAuthenticationManager"/>
<property name="securityMetadataSource" ref="resourceSecurityMetadataSource"/>
</bean> <!--方法拦截器 start-->
<bean id="methodSecurityInterceptor"
class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
<property name="accessDecisionManager" ref="accessDecisionManager"/>
<property name="authenticationManager" ref="myAuthenticationManager"/>
<property name="securityMetadataSource" ref="methodSecurityMetadataSource"/>
</bean>
<aop:config>
<aop:advisor advice-ref="methodSecurityInterceptor" pointcut="execution(* com.magic.rent.service.*.*(..))"
order="1"/>
</aop:config>
<!--方法拦截器 end--> <!--session管理器 start-->
<bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter">
<constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
<constructor-arg name="expiredUrl" value="/user/timeout"/>
</bean> <bean id="concurrentSessionControlStrategy"
class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
<constructor-arg name="sessionRegistry" ref="sessionRegistry"/>
<property name="maximumSessions" value="1"/>
</bean> <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/>
<!--session管理器 end-->
</beans>
applicationContext-security.xml
来吧,简单的,从头到尾的解释一下。
首先呢,最先看到的应该是过滤资源的配置:
<!--过滤资源 start-->
<!--不进行拦截的静态资源-->
<sec:http pattern="/css/*" security="none"/>
<sec:http pattern="/images/*" security="none"/>
<sec:http pattern="/images/**" security="none"/>
<sec:http pattern="/js/*" security="none"/>
<sec:http pattern="/fonts/*" security="none"/>
<!--不进行拦截的页面-->
<sec:http pattern="/WEB-INF/views/index.jsp" security="none"/>
<!--过滤资源 end-->
这些pattern意味着这些资源,不进行安全过滤,即在访问这些资源的时候,不需要进行Security的权限验证,举一个例子:在以“webapp”为根目录的情况下,css文件夹下的任何文件被访问将不进行安全验证,即任何用户都可以毫无顾忌的直接访问这些资源。
接下来的配置,相当重要,是整个框架的核心部分,如果不理解这部分,将无法好好使用这个框架。
<!--权限配置及自定义登录界面 start-->
<sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager">
<sec:form-login
login-page="/user/login"
login-processing-url="/login.do"
authentication-success-handler-ref="loginController"
authentication-failure-handler-ref="loginController"/>
<!--登出-->
<sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/>
<!--session管理及单点登录-->
<sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/>
<!--资源拦截器配置-->
<sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
<sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/>
</sec:http>
首先可以看到,“access-decision-manager-ref”是自定义框架的决策管理器(1),这个决策管理器是比如,当一个资源,被配置给3个不同的权限可以访问的时候,你可以决定,是只要拥有三个中的一个权限,就能访问资源,还是至少拥有2个权限,还是必须满足三个权限都拥有的情况下,才能访问资源。这就是决策管理器,就是制定放行规则。所以我们紧接着就要配置它了,这个决策管理器的配置,是这样的:
<!--决策管理器 start-->
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<constructor-arg name="decisionVoters">
<list>
<ref bean="roleVoter"/>
<ref bean="authenticatedVoter"/>
</list>
</constructor-arg>
<property name="messageSource" ref="messageSource"/>
</bean>
<bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter">
<property name="rolePrefix" value="ROLE_"/>
</bean>
<bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter"/>
<!--决策管理器 end-->
值得一提的是,bean:roleVoter中,有一个属性是”rolePrefix“,这个是用于设置角色前缀的。什么是角色前缀呢?先要解释什么是角色。SpringSecurity这个框架,默认的规则是以角色来判断是否有访问权限的,当然这并不符合我们的实际情况,我们使用的时候,更喜欢的是把角色更细化一层,比如,一个角色,具有多个“权限”,然后根据“权限”来判断是否有访问资源的资格。如果有资格,则访问,没资格,则返回403无权访问错误页面(当然默认的403有点丑,大部分情况我们都会对404、500、403这些常见的错误页面来去替换成我们自己编写的页面,这个回头再说。)。而角色权限,就是说,当系统读取到的一个字符串,判断它是否为一个用于表示角色的字符串,就是根据这个前缀来判断的,如果有心得朋友,可以查看“RoleVoter”这个类,可以发现,其实系统对rolePrefix设置了一个默认值,就是“ROLE_”,而我们在这里配置,只是我为了说明这个问题,当然我们可以通过配置Bean来修改这个前缀,不过我个人觉得这个“ROLE_”挺好的,就采用原有的了。那这边设置了前缀,就意味着,我们以后将角色存在数据库当中的时候,就必须给我们的角色定义这个前缀,比如我在数据库中存一个角色为管理员:ROLE_ADMIN。如果我们没有以约定好的前缀来定义角色,系统就会不识别,然后直接报无权限访问。这个也可以在RoleVoter这个类中的“supports”方法中得到查证。顺便说一下,框架会先调用这个supports方法,来校验是否是符合角色前缀的定义规则,如果不符合,根本都不进入后面的对比阶段,直接返回false,然后就被判定为无权访问了。可能就有朋友会想知道从哪里看出,先执行supports这个方法的,我在测试的时候,Debug了整个流程,但是现在已经不记得了,如果有想弄清楚的朋友,可以自行Debug,反正IDEA的Debug有记录整个执行过程,所以只需要在这个supports方法上打一个断点,然后查看上一个步骤就能找到调用的地方。
接着我们继续往下配置文件的下面看,
<sec:form-login
login-page="/user/login"
login-processing-url="/login.do"
authentication-success-handler-ref="loginController"
authentication-failure-handler-ref="loginController"/>
这里呢,定义了前台页面中,登录表单的一些规则,
- login-page:这个参数,配置的是登录页面的访问地址,因为我们是使用了SpringMVC,所以我自定义了一个Controller用于访问登录页面,而地址就是“/user/login”:
其实就是很简单的指向了login.jsp这个页面,也没有做什么其他的处理。- login-processing-url:这个参数呢,是当你在jsp或者html页面中,设计登录的表单<form>标签时,其中action元素的地址,就是你配置的这个参数,比如:
- authentication-success-handler-ref:这个参数,是定义一个当登录验证成功时要执行操作的控制器。
- authentication-failure-handler-ref:这个参数,是定一个,当登录验证失败时,要执行操作的控制器。
这两个参数,所对应的控制器,我为了简略,就把它们合并成为一个,这个控制器怎么写呢?实际很简单,登录验证成功的控制器呢,就是一个普通的java类,去实现AuthenticationSuccessHandler这个接口的方法“onAuthenticationSuccess”,而登录验证失败呢,就是实现AuthenticationFailureHandler的接口“anAuthenticationFailure”。我的实现类:
package com.magic.rent.controller; import com.magic.rent.pojo.SysUsers;
import com.magic.rent.service.IUserService;
import com.magic.rent.util.HttpUtil;
import com.magic.rent.util.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.Locale; public class LoginAuthenticationController implements AuthenticationSuccessHandler, AuthenticationFailureHandler, InitializingBean { @Autowired
private IUserService iUserService; private String successURL; private String failURL; private boolean byForward = false; private String AttrName; private String userInfo; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); private static Logger logger = LoggerFactory.getLogger(LoginAuthenticationController.class); public void setSuccessURL(String successURL) {
this.successURL = successURL;
} public void setFailURL(String failURL) {
this.failURL = failURL;
} public void setByForward(boolean byForward) {
this.byForward = byForward;
} public void setAttrName(String attrName) {
AttrName = attrName;
} public void setUserInfo(String userInfo) {
this.userInfo = userInfo;
} @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
SysUsers users;
JsonResult jsonResult;
try {
users = (SysUsers) authentication.getPrincipal();
Date date = new Date();
users.setLastLogin(date);
users.setLoginIp(HttpUtil.getIP(request));
try {
iUserService.updateUserLoginInfo(users);
} catch (DataAccessException e) {
logger.error("登录异常:保存登录数据失败!", e);
}
} catch (Exception e) {
jsonResult = JsonResult.error("用户登录信息保存失败!");
logger.error("登录异常:用户登录信息保存失败!", e);
request.getSession().setAttribute(AttrName, jsonResult);
return;
}
jsonResult = JsonResult.success("登录验证成功!", users);
request.getSession().setAttribute(userInfo, jsonResult);
httpReturn(request, response, true);
} @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class})
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
JsonResult jsonResult;
logger.info("登录失败:请求IP地址[{}];失败原因:{};", HttpUtil.getIP(request), exception.getMessage());
jsonResult = JsonResult.error(exception.getMessage());
request.getSession().setAttribute(AttrName, jsonResult);
httpReturn(request, response, false);
} public void afterPropertiesSet() throws Exception {
if (StringUtils.isEmpty(successURL))
throw new ExceptionInInitializerError("成功后跳转的地址未设置!");
if (StringUtils.isEmpty(failURL))
throw new ExceptionInInitializerError("失败后跳转的地址未设置!");
if (StringUtils.isEmpty(AttrName))
throw new ExceptionInInitializerError("Attr的Key值未设置!");
} private void httpReturn(HttpServletRequest request, HttpServletResponse response, boolean success) throws IOException, ServletException {
if (success) {
if (this.byForward) {
logger.info("登录成功:Forwarding to [{}]", successURL);
request.getRequestDispatcher(this.successURL).forward(request, response);
} else {
logger.info("登录成功:Redirecting to [{}]", successURL);
this.redirectStrategy.sendRedirect(request, response, this.successURL);
}
} else {
if (this.byForward) {
logger.info("登录失败:Forwarding to [{}]", failURL);
request.getRequestDispatcher(this.failURL).forward(request, response);
} else {
logger.info("登录失败:Redirecting to [{}]", failURL);
this.redirectStrategy.sendRedirect(request, response, this.failURL);
}
} }
}
估计还是需要简单解释一下,因为这个类我最终也是在Spring中装配的,所以一些字段我也就没有定义,只是做了get和set方法,等待配置。为了防止漏了这些字段的配置,所以我把这个类又另外实现了InitializingBean接口的afterPropertiesSet方法,这个方法可以在Spring框架启动,生产Bean对象对其属性进行装配的时候执行,然后我在这个方法中,对所有需要配置的属性,进行了非空验证。其实这个类的作用很简单,就是登陆成功后,保存登陆信息,然后跳转到登陆后的界面。对了,不能忘了这个LoginAuthenticationController的配置文件了:
<!--自定义验证结果控制器-->
<bean id="loginController"class="com.magic.rent.controller.LoginAuthenticationController">
<property name="successURL" value="/user/home"/>
<property name="failURL" value="/user/login"/>
<property name="attrName" value="loginResult"/>
<property name="byForward" value="false"/>
<property name="userInfo" value="userInfo"/>
</bean>
这配置应该算浅显易懂把,因为使用SpringMVC,所以每个地址其实都是SpringMVC的映射地址。
哦读了!上面那个类,有一个对象,就是JsonResult,这是我用于传输到前端的一个包装工具。
package com.magic.rent.util; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.io.Serializable; /**
* Created by wuxinzhe on 16/9/20.
*/
public class JsonResult implements Serializable { private static final long serialVersionUID = 8134245754393400511L; private boolean status = true;
private String message;
private Object data;
private static Logger logger = LoggerFactory.getLogger(JsonResult.class); public JsonResult() {
} public JsonResult(Object data) {
this.data = data;
} public boolean getStatus() {
return status;
} public JsonResult setStatus(boolean status) {
this.status = status;
return this;
} public String getMessage() {
return message;
} public JsonResult setMessage(String message) {
this.message = message;
return this;
} public Object getData() {
return data;
} public JsonResult setData(Object data) {
this.data = data;
return this;
} public static JsonResult success() {
return new JsonResult().setStatus(true);
} public static JsonResult success(Object data) {
JsonResult jsonResult = success().setData(data);
logger.info(jsonResult.toString());
return jsonResult;
} public static JsonResult success(String message, Object data) {
JsonResult jsonResult = success().setData(data).setMessage(message);
logger.info(jsonResult.toString());
return jsonResult;
} public static JsonResult error() {
return new JsonResult().setStatus(false);
} public static JsonResult error(String message) {
JsonResult jsonResult = error().setMessage(message);
logger.info(jsonResult.toString());
return jsonResult;
} @Override
public String toString() {
return "JsonResult{" +
"status=" + status +
", message='" + message + '\'' +
", data=" + data +
'}';
}
}
这个类还是跟朋友借鉴的呢,之前我也没有做过这种,不过这个说实话,真的很有用。