【JavaWeb】Spring+SpringMVC+MyBatis+SpringSecurity+EhCache+JCaptcha 完整Web基础框架(四)
SpringSecurity(1)
其实啊,这部分我是最不想写的,因为最麻烦的也是这部分,真的是非常非常的麻烦。关于SpringSecurity的配置,让我折腾了好半天,网上的配置方式一大把,但总有一些功能不完全,版本不是最新等等的问题在,所以几乎没有一个教程,是可以整个贯通的。当然我的意思不是说那些不好,那些也不错,但就对于我来说,还不够全面。另外,SpringSecurity的替代品是shiro,据说,两者的区别在于,前者涵盖的范围更广,但前者也相对学习成本更高。又因为SpringSecurity是Spring家族的成员之一,所以在Spring框架下应用的话,可以做到非常高度的自定义,算是非常灵活的安全框架,就是配置起来,真心复杂。
SpringSecurity的配置文件
目录:resource/config/spring,文件名:applicationContext-security.xml
1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:sec="http://www.springframework.org/schema/security" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans 7 http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 8 http://www.springframework.org/schema/aop 9 http://www.springframework.org/schema/aop/spring-aop.xsd 10 http://www.springframework.org/schema/security 11 http://www.springframework.org/schema/security/spring-security.xsd"> 12 13 <!--过滤资源 start--> 14 <!--不进行拦截的静态资源--> 15 <sec:http pattern="/css/*" security="none"/> 16 <sec:http pattern="/images/*" security="none"/> 17 <sec:http pattern="/images/**" security="none"/> 18 <sec:http pattern="/js/*" security="none"/> 19 <sec:http pattern="/fonts/*" security="none"/> 20 <!--不进行拦截的页面--> 21 <sec:http pattern="/WEB-INF/views/index.jsp" security="none"/> 22 <!--<sec:http pattern="WEB-INF/views/login.jsp" security="none"/>--> 23 <!--过滤资源 end--> 24 25 <!--权限配置及自定义登录界面 start--> 26 <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> 27 <sec:form-login 28 login-page="/user/login" 29 login-processing-url="/login.do" 30 authentication-success-handler-ref="loginController" 31 authentication-failure-handler-ref="loginController"/> 32 <!--登出--> 33 <sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/> 34 <!--session管理及单点登录--> 35 <sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/> 36 <!--资源拦截器配置--> 37 <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> 38 <sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/> 39 </sec:http> 40 41 <!--自定义验证结果控制器--> 42 <bean id="loginController" class="com.magic.rent.controller.LoginAuthenticationController"> 43 <property name="successURL" value="/user/home"/> 44 <property name="failURL" value="/user/login"/> 45 <property name="attrName" value="loginResult"/> 46 <property name="byForward" value="false"/> 47 <property name="userInfo" value="userInfo"/> 48 </bean> 49 50 <sec:authentication-manager alias="myAuthenticationManager"> 51 <sec:authentication-provider ref="daoAuthenticationProvider"/> 52 </sec:authentication-manager> 53 54 55 <!--权限查询服务--> 56 <bean id="cachingUserDetailsService" 57 class="org.springframework.security.config.authentication.CachingUserDetailsService"> 58 <constructor-arg name="delegate" ref="webUserDetailsService"/> 59 <property name="userCache"> 60 <bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache"> 61 <property name="cache" ref="userEhCacheFactory"/> 62 </bean> 63 </property> 64 </bean> 65 66 <bean id="daoAuthenticationProvider" 67 class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> 68 <property name="messageSource" ref="messageSource"/> 69 <property name="passwordEncoder" ref="messageDigestPasswordEncoder"/> 70 <property name="userDetailsService" ref="cachingUserDetailsService"/> 71 <property name="saltSource" ref="saltSource"/> 72 <property name="hideUserNotFoundExceptions" value="false"/> 73 </bean> 74 75 <!--MD5加密盐值--> 76 <bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource"> 77 <property name="userPropertyToUse" value="username"/> 78 </bean> 79 80 <!--决策管理器 start--> 81 <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> 82 <constructor-arg name="decisionVoters"> 83 <list> 84 <ref bean="roleVoter"/> 85 <ref bean="authenticatedVoter"/> 86 </list> 87 </constructor-arg> 88 <property name="messageSource" ref="messageSource"/> 89 </bean> 90 <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"> 91 <property name="rolePrefix" value="ROLE_"/> 92 </bean> 93 <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter"/> 94 <!--决策管理器 end--> 95 96 <!--资源拦截器 start--> 97 <bean id="filterSecurityInterceptor" 98 class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> 99 <property name="accessDecisionManager" ref="accessDecisionManager"/> 100 <property name="authenticationManager" ref="myAuthenticationManager"/> 101 <property name="securityMetadataSource" ref="resourceSecurityMetadataSource"/> 102 </bean> 103 104 <!--方法拦截器 start--> 105 <bean id="methodSecurityInterceptor" 106 class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> 107 <property name="accessDecisionManager" ref="accessDecisionManager"/> 108 <property name="authenticationManager" ref="myAuthenticationManager"/> 109 <property name="securityMetadataSource" ref="methodSecurityMetadataSource"/> 110 </bean> 111 <aop:config> 112 <aop:advisor advice-ref="methodSecurityInterceptor" pointcut="execution(* com.magic.rent.service.*.*(..))" 113 order="1"/> 114 </aop:config> 115 <!--方法拦截器 end--> 116 117 <!--session管理器 start--> 118 <bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> 119 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 120 <constructor-arg name="expiredUrl" value="/user/timeout"/> 121 </bean> 122 123 <bean id="concurrentSessionControlStrategy" 124 class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy"> 125 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 126 <property name="maximumSessions" value="1"/> 127 </bean> 128 129 <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/> 130 <!--session管理器 end--> 131 </beans>
来吧,简单的,从头到尾的解释一下。
首先呢,最先看到的应该是过滤资源的配置:
<!--过滤资源 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”。我的实现类:
1 package com.magic.rent.controller; 2 3 import com.magic.rent.pojo.SysUsers; 4 import com.magic.rent.service.IUserService; 5 import com.magic.rent.util.HttpUtil; 6 import com.magic.rent.util.JsonResult; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 import org.springframework.beans.factory.InitializingBean; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.context.MessageSource; 12 import org.springframework.context.support.MessageSourceAccessor; 13 import org.springframework.dao.DataAccessException; 14 import org.springframework.security.core.Authentication; 15 import org.springframework.security.core.AuthenticationException; 16 import org.springframework.security.web.DefaultRedirectStrategy; 17 import org.springframework.security.web.RedirectStrategy; 18 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 19 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 20 import org.springframework.transaction.annotation.Propagation; 21 import org.springframework.transaction.annotation.Transactional; 22 import org.springframework.util.StringUtils; 23 24 import javax.servlet.ServletException; 25 import javax.servlet.http.HttpServletRequest; 26 import javax.servlet.http.HttpServletResponse; 27 import java.io.IOException; 28 import java.util.Date; 29 import java.util.Locale; 30 31 public class LoginAuthenticationController implements AuthenticationSuccessHandler, AuthenticationFailureHandler, InitializingBean { 32 33 @Autowired 34 private IUserService iUserService; 35 36 private String successURL; 37 38 private String failURL; 39 40 private boolean byForward = false; 41 42 private String AttrName; 43 44 private String userInfo; 45 46 private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); 47 48 private static Logger logger = LoggerFactory.getLogger(LoginAuthenticationController.class); 49 50 public void setSuccessURL(String successURL) { 51 this.successURL = successURL; 52 } 53 54 public void setFailURL(String failURL) { 55 this.failURL = failURL; 56 } 57 58 public void setByForward(boolean byForward) { 59 this.byForward = byForward; 60 } 61 62 public void setAttrName(String attrName) { 63 AttrName = attrName; 64 } 65 66 public void setUserInfo(String userInfo) { 67 this.userInfo = userInfo; 68 } 69 70 @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class}) 71 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 72 SysUsers users; 73 JsonResult jsonResult; 74 try { 75 users = (SysUsers) authentication.getPrincipal(); 76 Date date = new Date(); 77 users.setLastLogin(date); 78 users.setLoginIp(HttpUtil.getIP(request)); 79 try { 80 iUserService.updateUserLoginInfo(users); 81 } catch (DataAccessException e) { 82 logger.error("登录异常:保存登录数据失败!", e); 83 } 84 } catch (Exception e) { 85 jsonResult = JsonResult.error("用户登录信息保存失败!"); 86 logger.error("登录异常:用户登录信息保存失败!", e); 87 request.getSession().setAttribute(AttrName, jsonResult); 88 return; 89 } 90 jsonResult = JsonResult.success("登录验证成功!", users); 91 request.getSession().setAttribute(userInfo, jsonResult); 92 httpReturn(request, response, true); 93 } 94 95 @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class}) 96 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { 97 JsonResult jsonResult; 98 logger.info("登录失败:请求IP地址[{}];失败原因:{};", HttpUtil.getIP(request), exception.getMessage()); 99 jsonResult = JsonResult.error(exception.getMessage()); 100 request.getSession().setAttribute(AttrName, jsonResult); 101 httpReturn(request, response, false); 102 } 103 104 public void afterPropertiesSet() throws Exception { 105 if (StringUtils.isEmpty(successURL)) 106 throw new ExceptionInInitializerError("成功后跳转的地址未设置!"); 107 if (StringUtils.isEmpty(failURL)) 108 throw new ExceptionInInitializerError("失败后跳转的地址未设置!"); 109 if (StringUtils.isEmpty(AttrName)) 110 throw new ExceptionInInitializerError("Attr的Key值未设置!"); 111 } 112 113 private void httpReturn(HttpServletRequest request, HttpServletResponse response, boolean success) throws IOException, ServletException { 114 if (success) { 115 if (this.byForward) { 116 logger.info("登录成功:Forwarding to [{}]", successURL); 117 request.getRequestDispatcher(this.successURL).forward(request, response); 118 } else { 119 logger.info("登录成功:Redirecting to [{}]", successURL); 120 this.redirectStrategy.sendRedirect(request, response, this.successURL); 121 } 122 } else { 123 if (this.byForward) { 124 logger.info("登录失败:Forwarding to [{}]", failURL); 125 request.getRequestDispatcher(this.failURL).forward(request, response); 126 } else { 127 logger.info("登录失败:Redirecting to [{}]", failURL); 128 this.redirectStrategy.sendRedirect(request, response, this.failURL); 129 } 130 } 131 132 } 133 }
估计还是需要简单解释一下,因为这个类我最终也是在Spring中装配的,所以一些字段我也就没有定义,只是做了get和set方法,等待配置。为了防止漏了这些字段的配置,所以我把这个类又另外实现了InitializingBean接口的afterPropertiesSet方法,这个方法可以在Spring框架启动,生产Bean对象对其属性进行装配的时候执行,然后我在这个方法中,对所有需要配置的属性,进行了非空验证。其实这个类的作用很简单,就是登陆成功后,保存登陆信息,然后跳转到登陆后的界面。对了,不能忘了这个LoginAuthenticationController的配置文件了:
1 <!--自定义验证结果控制器--> 2 <bean id="loginController"class="com.magic.rent.controller.LoginAuthenticationController"> 3 <property name="successURL" value="/user/home"/> 4 <property name="failURL" value="/user/login"/> 5 <property name="attrName" value="loginResult"/> 6 <property name="byForward" value="false"/> 7 <property name="userInfo" value="userInfo"/> 8 </bean>
这配置应该算浅显易懂把,因为使用SpringMVC,所以每个地址其实都是SpringMVC的映射地址。
哦读了!上面那个类,有一个对象,就是JsonResult,这是我用于传输到前端的一个包装工具。
1 package com.magic.rent.util; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import java.io.Serializable; 7 8 /** 9 * Created by wuxinzhe on 16/9/20. 10 */ 11 public class JsonResult implements Serializable { 12 13 private static final long serialVersionUID = 8134245754393400511L; 14 15 private boolean status = true; 16 private String message; 17 private Object data; 18 private static Logger logger = LoggerFactory.getLogger(JsonResult.class); 19 20 public JsonResult() { 21 } 22 23 public JsonResult(Object data) { 24 this.data = data; 25 } 26 27 public boolean getStatus() { 28 return status; 29 } 30 31 public JsonResult setStatus(boolean status) { 32 this.status = status; 33 return this; 34 } 35 36 public String getMessage() { 37 return message; 38 } 39 40 public JsonResult setMessage(String message) { 41 this.message = message; 42 return this; 43 } 44 45 public Object getData() { 46 return data; 47 } 48 49 public JsonResult setData(Object data) { 50 this.data = data; 51 return this; 52 } 53 54 public static JsonResult success() { 55 return new JsonResult().setStatus(true); 56 } 57 58 public static JsonResult success(Object data) { 59 JsonResult jsonResult = success().setData(data); 60 logger.info(jsonResult.toString()); 61 return jsonResult; 62 } 63 64 public static JsonResult success(String message, Object data) { 65 JsonResult jsonResult = success().setData(data).setMessage(message); 66 logger.info(jsonResult.toString()); 67 return jsonResult; 68 } 69 70 public static JsonResult error() { 71 return new JsonResult().setStatus(false); 72 } 73 74 public static JsonResult error(String message) { 75 JsonResult jsonResult = error().setMessage(message); 76 logger.info(jsonResult.toString()); 77 return jsonResult; 78 } 79 80 @Override 81 public String toString() { 82 return "JsonResult{" + 83 "status=" + status + 84 ", message='" + message + '\'' + 85 ", data=" + data + 86 '}'; 87 } 88 }
这个类还是跟朋友借鉴的呢,之前我也没有做过这种,不过这个说实话,真的很有用。