(14)—- Logout和SessionManager

最后更新于:2022-04-01 14:54:24

Logout的配置很简单,只需要在http中加入下面的配置就可以了 ~~~ <sec:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" /> ~~~ invalidate-session是否销毁Session logout-url logout地址 logout-success-url logout成功后要跳转的地址 Session管理中最简单的配置方法是 ~~~ <sec:session-management invalid-session-url="/login.jsp" /> ~~~ 意思就是Session失效时跳转到login.jsp 配置同一事件,只能有一个用户登录系统。 网上有的例子是这样配置的 ~~~ <sec:session-management invalid-session-url="/login.jsp" > <sec:concurrency-control error-if-maximum-exceeded="true" max-sessions="1" expired-url="/login.jsp"/> </sec:session-management> ~~~ 但是这种配置在3.2版本中不管用 在3.2版本中需要这样配置 首先在web.xml中加入一下配置 ~~~ <listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener> ~~~ 然后修改applicationContext-security.xml ~~~ <sec:http access-decision-manager-ref="accessDecisionManager" entry-point-ref="authenticationEntryPoint"> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" /> <sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy" /> <sec:remember-me authentication-success-handler-ref="authenticationSuccessHandler" data-source-ref="dataSource" user-service-ref="userDetailService" /> <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> <sec:custom-filter ref="captchaAuthenticaionFilter" position="FORM_LOGIN_FILTER"/> <sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/> </sec:http> <bean id="captchaAuthenticaionFilter" class="com.zrhis.system.security.CaptchaAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager" /> <property name="authenticationFailureHandler" ref="authenticationFailureHandler" /> <property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" /> <property name="filterProcessesUrl" value="/login.do" /> <property name="sessionAuthenticationStrategy" ref="concurrentSessionControlStrategy" /> </bean> <bean id="authenticationSuccessHandler" class="com.zrhis.system.security.SavedRequestLoginSuccessHandler"> <property name="defaultTargetUrl" value="/index.jsp" /> <property name="forwardToDestination" value="true" /> <property name="alwaysUseDefaultTargetUrl" value="false" /> </bean> <bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/login.jsp" /> </bean> <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="loginFormUrl" value="/login.jsp" /> </bean> <bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> <constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <constructor-arg name="expiredUrl" value="/sessionOut.jsp" /> </bean> <bean id="concurrentSessionControlStrategy" class="org.springframework.security.web.authentication.session.ConcurrentSessionControlStrategy"> <constructor-arg name="sessionRegistry" ref="sessionRegistry" /> <property name="maximumSessions" value="1"></property> </bean> <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl" /> ~~~
';

(13)—- 验证码功能的实现

最后更新于:2022-04-01 14:54:21

有三中方法可以实现验证码的功能 第一种是自定义一个filter,放在SpringSecurity过滤器之前,在用户登录的时候会先经过这个filter,然后在这个filter中实现对验证码进行验证的功能,这种方法不推荐,因为它已经脱离了SpringSecurity 第二种是自定义一个filter让它继承自UsernamePasswordAuthenticationFilter,然后重写attemptAuthentication方法在这个方法中实现验证码的功能,如果验证码错误就抛出一个继承自AuthenticationException的验证吗错误的异常比如(CaptchaException),然后这个异常就会被SpringSecurity捕获到并将异常信息返回到前台,这种实现起来比较简单 ~~~ @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String requestCaptcha = request.getParameter(this.getCaptchaFieldName()); String genCaptcha = (String)request.getSession().getAttribute("code"); logger.info("开始校验验证码,生成的验证码为:"+genCaptcha+" ,输入的验证码为:"+requestCaptcha); if( !genCaptcha.equals(requestCaptcha)){ throw new CaptchaException( this.messageSource.getMessage("AbstractUserDetailsAuthenticationProvider.badCaptcha",null,"Default",null)); } return super.attemptAuthentication(request, response); } ~~~ 然后在配置文件中配置下 ~~~ <bean id="loginFilter" class="com.zrhis.system.security.DefaultUsernamePasswordAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager"></property> <property name="authenticationSuccessHandler"> <bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler"> <property name="defaultTargetUrl" value="/index.jsp"></property> </bean> </property> <property name="authenticationFailureHandler"> <bean class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/login.jsp"></property> </bean> </property> </bean> ~~~ 最后在http中加入custom-filter配置,将这个filter放在SpringSecurity的FORM_LOGIN_FILTER之前 ~~~ <custom-filter ref="loginFilter" before="FORM_LOGIN_FILTER"/> ~~~ 最后一种是直接替换掉SpringSecurity的UsernamePasswordAuthenticationFilter,这种比较复杂,但是更为合理,也是我现在正在用的。 如果用这种方法那么http 中的auto-config就必须去掉,而form-login配置也必须去掉,因为这个不需要了,里面的属性都需要我们自行注入。 首先需要创建一个EntryPoint ~~~ <bean id="authenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint"> <property name="loginFormUrl" value="/login.jsp" /> </bean> ~~~ 然后在http中配置下 ~~~ <sec:http access-decision-manager-ref="accessDecisionManager" entry-point-ref="authenticationEntryPoint"> ~~~ 然后我们来写CaptchaAuthenticationFilter,同样需要继承自UsernamePasswordAuthenticationFilter ~~~ public class CaptchaAuthenticationFilter extends UsernamePasswordAuthenticationFilter{ public static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "j_captcha"; public static final String SESSION_GENERATED_CAPTCHA_KEY = Constant.SESSION_GENERATED_CAPTCHA_KEY; private String captchaParameter = SPRING_SECURITY_FORM_CAPTCHA_KEY; public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String genCode = this.obtainGeneratedCaptcha(request); String inputCode = this.obtainCaptcha(request); if(genCode == null) throw new CaptchaException(this.messages.getMessage("LoginAuthentication.captchaInvalid")); if(!genCode.equalsIgnoreCase(inputCode)){ throw new CaptchaException(this.messages.getMessage("LoginAuthentication.captchaNotEquals")); } return super.attemptAuthentication(request, response); } protected String obtainCaptcha(HttpServletRequest request){ return request.getParameter(this.captchaParameter); } protected String obtainGeneratedCaptcha (HttpServletRequest request){ return (String)request.getSession().getAttribute(SESSION_GENERATED_CAPTCHA_KEY); } } ~~~ 在配置文件中配置CaptchaAuthenticationFilter ~~~ <bean id="captchaAuthenticaionFilter" class="com.zrhis.system.security.CaptchaAuthenticationFilter"> <property name="authenticationManager" ref="authenticationManager" /> <property name="authenticationFailureHandler" ref="authenticationFailureHandler" /> <property name="authenticationSuccessHandler" ref="authenticationSuccessHandler" /> <property name="filterProcessesUrl" value="/login.do" /> </bean> <bean id="authenticationSuccessHandler" class="com.zrhis.system.security.SimpleLoginSuccessHandler"> <property name="defaultTargetUrl" value="/WEB-INF/app.jsp"></property> <property name="forwardToDestination" value="true"></property> </bean> <bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <property name="defaultFailureUrl" value="/login.jsp" /> </bean> ~~~ 从配置文件中就可以看出来authenticationManager、authenticationFailureHandler、authenticationSuccessHandler、filterProcessesUrl等都需要我们自行注入了。 filterProcessesUrl定义的是登录验证的地址,默认的是j_spring_security_check这里我们改成login.do authenticationSuccessHandler中的defaultTargetUrl定义的是登录成功后跳转到的页面 authenticationFailureHandler中的defaultTargetUrl定义的是登录失败后跳转到的页面 我们的首页app.jsp在/WEB-INF下所以需要使用服务器跳转,所以需要将forwardToDestination设为true,因为客户端跳转是不能直接访问WEB-INF下的内容的。 最后在http中将FORM_LOGIN_FILTER替换掉,最终http中完整的配置就变成了下面的内容 ~~~ <sec:http access-decision-manager-ref="accessDecisionManager" entry-point-ref="authenticationEntryPoint"> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:session-management invalid-session-url="/login.jsp" /> <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> <sec:custom-filter ref="captchaAuthenticaionFilter" position="FORM_LOGIN_FILTER"/> </sec:http> ~~~ custom-filter中before是在这个filter之前,after是之后,position是替换。 这样就可以实现对验证码的验证了,效果如下 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b2970a57.jpg)
';

(12)—- 使用数据库来管理方法

最后更新于:2022-04-01 14:54:19

这个稍微有一点复杂,我是通过AOP来实现的,前半部分跟上一章类似,主要在配置上有点不同 读取方法与权限对应列表DAO ~~~ public List<Map<String,String>> getMethodResourceMapping(){ String sql = "SELECT S3.RESOURCE_PATH,S2.AUTHORITY_MARK FROM SYS_AUTHORITIES_RESOURCES S1 "+ "JOIN SYS_AUTHORITIES S2 ON S1.AUTHORITY_ID = S2.AUTHORITY_ID "+ "JOIN SYS_RESOURCES S3 ON S1.RESOURCE_ID = S3.RESOURCE_ID AND S3.RESOURCE_TYPE='METHOD' ORDER BY S3.PRIORITY DESC"; List<Map<String,String>> list = new ArrayList<Map<String,String>>(); Query query = this.entityManager.createNativeQuery(sql); List<Object[]> result = query.getResultList(); Iterator<Object[]> it = result.iterator(); while(it.hasNext()){ Object[] o = it.next(); Map<String,String> map = new HashMap<String,String>(); map.put("resourcePath", (String)o[0]); map.put("authorityMark", (String)o[1]); list.add(map); } return list; } ~~~ 这里只针对方法名拦截,对于同一个类中的同名方法统统作为一个方法拦截。如果需要更细粒度的拦截,即拦截到参数,请在完成这个内容之后自行进行深入的研究。 MethodKey用来做主键用的 ~~~ public class MethodKey { private String className; private String methodName; public MethodKey(){}; public MethodKey(String fullName){ this.className = StringUtils.stripFilenameExtension(fullName); this.methodName = StringUtils.getFilenameExtension(fullName); }; public MethodKey(Method method) { super(); this.className = method.getDeclaringClass().getName(); this.methodName = method.getName(); } public String getClassName() { return className; } public void setClassName(String className) { this.className = className; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } public String getFullMethodName(){ return this.className + "." + this.methodName; } @Override public boolean equals(Object obj) { if(!(obj instanceof MethodKey))return false; MethodKey target = (MethodKey)obj; if(this.className.equals(target.getClassName()) && this.methodName.equals(target.getMethodName()))return true; return false; } } ~~~ getDeclaringClass获取到的是接口,这里我们只拦截接口。 MethodSecurityMetadataSource ~~~ public class MethodSecurityMetadataSource extends AbstractMethodSecurityMetadataSource implements InitializingBean{ //protected Log logger = LogFactory.getLog(getClass()); private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = Collections.emptyList(); private final static String RES_KEY = "resourcePath"; private final static String AUTH_KEY = "authorityMark"; private Map<MethodKey, Collection<ConfigAttribute>> requestMap; @Autowired private SysResourceRepository sysResourceRepository; /** * 根据方法获取到访问方法所需要的权限 * @param method 访问的方法 * @param targetClass 方法所属的类 */ @Override public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) { MethodKey key = new MethodKey(method); Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE; for (Map.Entry<MethodKey, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().equals(key)) { attrs = entry.getValue(); break; } } logger.info("METHOD资源:"+key.getFullMethodName()+ " -> " +attrs); return attrs; } /** * 获取到所有方法对应的权限集合 */ @Override public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Map.Entry<MethodKey, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { allAttributes.addAll(entry.getValue()); } return allAttributes; } /** * 初始化方法权限对应集合,绑定方法权限集合 */ @Override public void afterPropertiesSet() throws Exception { this.requestMap = this.bindRequestMap(); } /** * 从数据库中获取方法及权限对应信息 * @return */ private Map<String,String> loadMehod(){ Map<String,String> resMap = new LinkedHashMap<String, String>(); List<Map<String,String>> list = this.sysResourceRepository.getMethodResourceMapping(); for(Map<String,String> map : list){ String resourcePath = map.get(RES_KEY); String authorityMark = map.get(AUTH_KEY); if(resMap.containsKey(resourcePath)){ String mark = resMap.get(resourcePath); resMap.put(resourcePath, mark+","+authorityMark); }else{ resMap.put(resourcePath, authorityMark); } } return resMap; } /** * 封装从数据库中获取的方法权限集合 * @return */ public Map<MethodKey, Collection<ConfigAttribute>> bindRequestMap(){ Map<MethodKey, Collection<ConfigAttribute>> resMap = new LinkedHashMap<MethodKey, Collection<ConfigAttribute>>(); Map<String,String> map = this.loadMehod(); for(Map.Entry<String, String> entry : map.entrySet()){ MethodKey key = new MethodKey(entry.getKey()); Collection<ConfigAttribute> atts = SecurityConfig.createListFromCommaDelimitedString(entry.getValue()); resMap.put(key, atts); } return resMap; } } ~~~ 与资源的SecurityMetadataSource类似,只不过拦截方法的SecurityMetadataSource需要继承自AbstractMethodSecurityMetadataSource或实现MethodSecurityMetadataSource 具体配置 ~~~ <bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> <property name="accessDecisionManager" ref="accessDecisionManager" /> <property name="authenticationManager" ref="authenticationManager" /> <property name="securityMetadataSource" ref="methodSecurityMetadataSource" /> </bean> <bean id="methodSecurityMetadataSource" class="com.zrhis.system.security.MethodSecurityMetadataSource" /> <aop:config> <aop:pointcut id="sevicePointcut" expression="execution(* com.zrhis.**.service.*.*(..))"/> <aop:advisor advice-ref="methodSecurityInterceptor" pointcut-ref="sevicePointcut" order="1"/> </aop:config> ~~~ 首先创建pointcut,pointcut是项目中的Service层。然后在advisor中配置拦截器及切面的对应关系 方法的拦截是通过AOP来实现的,方法的拦截到此结束 有资源的拦截和方法的拦截基本上就能保证项目的权限能够灵活分配了。
';

(11)—- 使用数据库来管理资源

最后更新于:2022-04-01 14:54:17

这个可以说是SpringSecurity最核心的东西,在项目中资源很多肯定不能一一配置到配置文件中,所以用数据库来管理资源是必然的。这个也很容易实现。表结构已经在之前都创建过了。 首先我们要来从数据库中获取到资源与权限的对应列表,这个在dao层实现即可需要获取到url地址和AUTH_**这种权限标识,注意:不是权限ID和资源ID。 ~~~ public List<Map<String,String>> getURLResourceMapping(){ String sql = "SELECT S3.RESOURCE_PATH,S2.AUTHORITY_MARK FROM SYS_AUTHORITIES_RESOURCES S1 "+ "JOIN SYS_AUTHORITIES S2 ON S1.AUTHORITY_ID = S2.AUTHORITY_ID "+ "JOIN SYS_RESOURCES S3 ON S1.RESOURCE_ID = S3.RESOURCE_ID S3.RESOURCE_TYPE='URL' ORDER BY S3.PRIORITY DESC"; List<Map<String,String>> list = new ArrayList<Map<String,String>>(); Query query = this.entityManager.createNativeQuery(sql); List<Object[]> result = query.getResultList(); Iterator<Object[]> it = result.iterator(); while(it.hasNext()){ Object[] o = it.next(); Map<String,String> map = new HashMap<String,String>(); map.put("resourcePath", (String)o[0]); map.put("authorityMark", (String)o[1]); list.add(map); } return list; } ~~~ 创建SecurityMetadataSource供过滤器使用,SecurityMetadataSource需要实现FilterInvocationSecurityMetadataSource ~~~ public class URLFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource,InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = Collections.emptyList(); //权限集合 private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap; @Autowired private SysResourceRepository sysResourceRepository; /* (non-Javadoc) * @see org.springframework.security.access.SecurityMetadataSource#getAttributes(java.lang.Object) */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { final HttpServletRequest request = ((FilterInvocation) object).getRequest(); Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE; for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().matches(request)) { attrs = entry.getValue(); break; } } logger.info("URL资源:"+request.getRequestURI()+ " -> " + attrs); return attrs; } /* (non-Javadoc) * @see org.springframework.security.access.SecurityMetadataSource#getAllConfigAttributes() */ @Override public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { allAttributes.addAll(entry.getValue()); } return allAttributes; } /* (non-Javadoc) * @see org.springframework.security.access.SecurityMetadataSource#supports(java.lang.Class) */ @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } private Map<String,String> loadResuorce(){ Map<String,String> map = new LinkedHashMap<String,String>(); List<Map<String,String>> list = this.sysResourceRepository.getURLResourceMapping(); Iterator<Map<String,String>> it = list.iterator(); while(it.hasNext()){ Map<String,String> rs = it.next(); String resourcePath = rs.get("resourcePath"); String authorityMark = rs.get("authorityMark"); if(map.containsKey(resourcePath)){ String mark = map.get("resourcePath"); map.put(resourcePath, mark+","+authorityMark); }else{ map.put(resourcePath, authorityMark); } } return map; } protected Map<RequestMatcher, Collection<ConfigAttribute>> bindRequestMap(){ Map<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(); Map<String,String> resMap = this.loadResuorce(); for(Map.Entry<String,String> entry:resMap.entrySet()){ String key = entry.getKey(); Collection<ConfigAttribute> atts = new ArrayList<ConfigAttribute>(); atts = SecurityConfig.createListFromCommaDelimitedString(entry.getValue()); map.put(new AntPathRequestMatcher(key), atts); } return map; } /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @Override public void afterPropertiesSet() throws Exception { this.requestMap = this.bindRequestMap(); logger.info("资源权限列表"+this.requestMap); } public void refreshResuorceMap(){ this.requestMap = this.bindRequestMap(); } } ~~~ bindRequestMap需要在类初始化的时候就完成,但是这个不能写在构造函数中,因为构造函数执行是SysResourceRepository还没有注入过来。所以就通过实现InitializingBean把初始化操作放在afterPropertiesSet方法中。 getAllConfigAttributes:获取所有权限集合 getAttributes:根据request请求获取访问资源所需权限 代码很简单,很容易看懂,就不再多做解释,下面看配置文件 ~~~ <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:session-management invalid-session-url="/login.jsp" /> <sec:form-login login-page="/login.jsp" login-processing-url="/login.do" authentication-failure-url="/login.jsp" authentication-success-handler-ref="authenticationSuccessHandler" /> <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> </sec:http> <bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> <property name="accessDecisionManager" ref="accessDecisionManager" /> <property name="authenticationManager" ref="authenticationManager" /> <property name="securityMetadataSource" ref="securityMetadataSource" /> </bean> <bean id="securityMetadataSource" class="com.zrhis.system.security.URLFilterInvocationSecurityMetadataSource"/> ~~~ 通过配置custom-filter来增加过滤器,before="FILTER_SECURITY_INTERCEPTOR"表示在SpringSecurity默认的过滤器之前执行。 FilterSecurityInterceptor还用SpringSecurity默认的就可以了,这个是没有必要自己写的只要在SecurityMetadataSource处理好资源与权限的对应关系就可以了。 到此为止SpringSecurity框架已基本完善,可以说在项目中用已经没什么问题了。
';

(10)—- 自定义登录成功后的处理程序及修改默认验证地址

最后更新于:2022-04-01 14:54:14

form-login配置中的authentication-success-handler-ref可以让手动注入登录成功后的处理程序,需要实现AuthenticationSuccessHandler接口。 ~~~ <sec:form-login login-page="/login.jsp" login-processing-url="/login.do" authentication-failure-url="/login.jsp" authentication-success-handler-ref="authenticationSuccessHandler" /> ~~~ springSecurity默认的登录用户验证路径为:j_spring_security_check,这个路径是可以通过login-processing-url来自己定义,比如我的就定义成了login.do。 然后在前台登录页面中改下form中的action就可以了。 配置文件,注意这里的defaultTargetUrl,本来这个是在form-login中,配置的但是如果我们自己定义登录成功后的处理程序后它就不起作用了,所以这个跳转也需要我们在自定义程序中处理。 ~~~ <bean id="authenticationSuccessHandler" class="com.zrhis.system.security.SimpleLoginSuccessHandler"> <property name="defaultTargetUrl" value="/WEB-INF/app.jsp"></property> <property name="forwardToDestination" value="true"></property> </bean> ~~~ SimpleLoginSuccessHandler,这个类主要处理登录后的处理,我处理的是登录后记录用户的IP地址和登录时间,代码如下  ~~~ package com.zrhis.system.security; import java.io.IOException; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.security.core.Authentication; import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; 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 com.zrhis.base.exception.InitializationException; import com.zrhis.system.bean.SysUsers; import com.zrhis.system.repository.SysUsersRepository; public class SimpleLoginSuccessHandler implements AuthenticationSuccessHandler,InitializingBean { protected Log logger = LogFactory.getLog(getClass()); private String defaultTargetUrl; private boolean forwardToDestination = false; private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @Autowired private SysUsersRepository sysUsersRepository; /* (non-Javadoc) * @see org.springframework.security.web.authentication.AuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication) */ @Override @Transactional(readOnly=false,propagation= Propagation.REQUIRED,rollbackFor={Exception.class}) public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { this.saveLoginInfo(request, authentication); if(this.forwardToDestination){ logger.info("Login success,Forwarding to "+this.defaultTargetUrl); request.getRequestDispatcher(this.defaultTargetUrl).forward(request, response); }else{ logger.info("Login success,Redirecting to "+this.defaultTargetUrl); this.redirectStrategy.sendRedirect(request, response, this.defaultTargetUrl); } } @Transactional(readOnly=false,propagation= Propagation.REQUIRED,rollbackFor={Exception.class}) public void saveLoginInfo(HttpServletRequest request,Authentication authentication){ SysUsers user = (SysUsers)authentication.getPrincipal(); try { String ip = this.getIpAddress(request); Date date = new Date(); user.setLastLogin(date); user.setLoginIp(ip); this.sysUsersRepository.saveAndFlush(user); } catch (DataAccessException e) { if(logger.isWarnEnabled()){ logger.info("无法更新用户登录信息至数据库"); } } } public String getIpAddress(HttpServletRequest request){ String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } public void setDefaultTargetUrl(String defaultTargetUrl) { this.defaultTargetUrl = defaultTargetUrl; } public void setForwardToDestination(boolean forwardToDestination) { this.forwardToDestination = forwardToDestination; } /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @Override public void afterPropertiesSet() throws Exception { if(StringUtils.isEmpty(defaultTargetUrl)) throw new InitializationException("You must configure defaultTargetUrl"); } } ~~~ 其中getIpAddress方法摘自网络,如有雷同纯属必然。 实现InitializingBean,在afterPropertiesSet中我们验证defaultTargetUrl是否为空,如果为空就抛出异常,因为这个地址是必须的。可以根据自己的情况来选择要不要加验证。 如果实现InitializingBean在程序启动是Spring在创建完这个类并注入属性后会自动执行afterPropertiesSet,所以我们的一些初始化的操作也是可以在这里完成的。 onAuthenticationSuccess是主要的接口这个是登录成功后Spring调用的方法,而我们的跳转和保存用户信息都是在这里完成的。 RedirectStrategy是Spring提供的一个客户端跳转的工具类。使用它可以支持“/index.jsp”这种地址,同时可以保证服务器跳转和客户端跳转的路径一致。 加入我们的项目名为my ,项目访问地址为http://localhost:8080/my 现在要使用客户端跳转到 "/login.jsp" 如果是response.sendRedirect 会直接跳转到http://localhost:8080/login.jsp 而使用redirectStrategy.sendRedirect则会跳转到http://localhost:8080/my/login.jsp 在redirectStrategy中,'/'代表的是项目根目录而不是服务器根目录。
';

(9)—- 自定义AccessDeniedHandler

最后更新于:2022-04-01 14:54:12

在Spring默认的AccessDeniedHandler中只有对页面请求的处理,而没有对Ajax的处理。而在项目开发是Ajax又是我们要常用的技术,所以我们可以通过自定义AccessDeniedHandler来处理Ajax请求。我们在Spring默认的AccessDeniedHandlerImpl上稍作修改就可以了。 ~~~ public class DefaultAccessDeniedHandler implements AccessDeniedHandler { /* (non-Javadoc) * @see org.springframework.security.web.access.AccessDeniedHandler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.access.AccessDeniedException) */ private String errorPage; //~ Methods ======================================================================================================== public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { boolean isAjax = ControllerTools.isAjaxRequest(request); if(isAjax){ Message msg = MessageManager.exception(accessDeniedException); ControllerTools.print(response, msg); }else if (!response.isCommitted()) { if (errorPage != null) { // Put exception into request scope (perhaps of use to a view) request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); // Set the 403 status code. response.setStatus(HttpServletResponse.SC_FORBIDDEN); // forward to error page. RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } } /** * The error page to use. Must begin with a "/" and is interpreted relative to the current context root. * * @param errorPage the dispatcher path to display * * @throws IllegalArgumentException if the argument doesn't comply with the above limitations */ public void setErrorPage(String errorPage) { if ((errorPage != null) && !errorPage.startsWith("/")) { throw new IllegalArgumentException("errorPage must begin with '/'"); } this.errorPage = errorPage; } } ~~~ 这里我们直接将异常信息通过PrintWriter输出到前台,然后在前台做统一的处理就可以了。在前台对后台消息统一处理的方法可以参考我的这篇文章[http://blog.csdn.net/jaune161/article/details/18135607](http://blog.csdn.net/jaune161/article/details/18135607) 最后在配置文件中配置下 ~~~ <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:session-management invalid-session-url="/login.jsp" /> <sec:intercept-url pattern="/app.jsp" access="AUTH_LOGIN"/> <sec:intercept-url pattern="/**" access="AUTH_GG_FBGBGG"/> <sec:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp" default-target-url="/index.jsp"/> </sec:http> <!-- 自定义权限不足处理程序 --> <bean id="accessDeniedHandler" class="com.zrhis.system.security.RequestAccessDeniedHandler"> <property name="errorPage" value="/WEB-INF/error/403.jsp"></property> </bean> ~~~ session-management本来计划在之前就讲的,但是准备深入讲下session-management所以就一直没有讲。今天既然提到了就简单的说下session-management最简单的配置,就是上面的配置invalid-session-url表示Session失效时跳转的连接。随后会深入讲下这个。
';

(8)—- 自定义决策管理器及修改权限前缀

最后更新于:2022-04-01 14:54:10

首先介绍下Spring的决策管理器,其接口为AccessDecisionManager,抽象类为AbstractAccessDecisionManager。而我们要自定义决策管理器的话一般是继承抽象类而不去直接实现接口。 在Spring中引入了投票器(AccessDecisionVoter)的概念,有无权限访问的最终觉得权是由投票器来决定的,最常见的投票器为RoleVoter,在RoleVoter中定义了权限的前缀,先看下Spring在RoleVoter中是怎么处理授权的。 ~~~ public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) { int result = ACCESS_ABSTAIN; Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication); for (ConfigAttribute attribute : attributes) { if (this.supports(attribute)) { result = ACCESS_DENIED; // Attempt to find a matching granted authority for (GrantedAuthority authority : authorities) { if (attribute.getAttribute().equals(authority.getAuthority())) { return ACCESS_GRANTED; } } } } return result; } Collection<? extends GrantedAuthority> extractAuthorities(Authentication authentication) { return authentication.getAuthorities(); } ~~~ Authentication中是用户及用户权限信息,attributes是访问资源需要的权限,然后循环判断用户是否有访问资源需要的权限,如果有就返回ACCESS_GRANTED,通俗的说就是有权限。 Spring提供了3个决策管理器,至于这三个管理器是如何工作的请查看SpringSecurity源码 AffirmativeBased 一票通过,只要有一个投票器通过就允许访问 ConsensusBased 有一半以上投票器通过才允许访问资源 UnanimousBased 所有投票器都通过才允许访问 下面来实现一个简单的自定义决策管理器,这个决策管理器并没有使用投票器 ~~~ public class DefaultAccessDecisionManager extends AbstractAccessDecisionManager { public void decide( Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException{ SysUser user = (SysUser)authentication.getPrincipal(); logger.info("访问资源的用户为"+user.getUsername()); //如果访问资源不需要任何权限则直接通过 if( configAttributes == null ) { return ; } Iterator<ConfigAttribute> ite = configAttributes.iterator(); //遍历configAttributes看用户是否有访问资源的权限 while( ite.hasNext()){ ConfigAttribute ca = ite.next(); String needRole = ((SecurityConfig)ca).getAttribute(); //ga 为用户所被赋予的权限。 needRole 为访问相应的资源应该具有的权限。 for( GrantedAuthority ga: authentication.getAuthorities()){ if(needRole.trim().equals(ga.getAuthority().trim())){ return; } } } throw new AccessDeniedException(""); } } ~~~ decide这个方法没有任何的返回值,需要在没有通过授权时抛出AccessDeniedException。 如果有访问某个资源需要同时拥有两个或两个以上权限的情况,这时候就要通过自定义AccessDecisionVoter来实现了,这个也很简单在这里就不赘述了。如果要在页面中使用hasRole()这样的表达式就需要注入WebExpressionVoter了。 在SpringSecurity中自定义权限前缀 权限的前缀默认是ROLE_,网上的很多例子是说,直接在配置文件中加上下面的配置就可以了。 ~~~ <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"> <property name="rolePrefix" value="AUTH_"></property> </bean> ~~~ 亲测不管用的,我想应该不是我配置的问题,而是在我们配置了http auto-config="true"Spring就已经将AccessDecisionManager初始化好了,即便配置到之前也不行,因为这个初始化是Spring自己来完成的,它并没有把你配置的roleVoter注入到AccessDecisionManager中。那我们就来手动的注入AccessDecisionManager吧。 在http配置中有个access-decision-manager-ref属性,可以使我们手动注入AccessDecisionManager,下面是详细配置 ~~~ <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:session-management invalid-session-url="/login.jsp" /> <sec:intercept-url pattern="/app.jsp" access="AUTH_GG_FBGBGG"/> <sec:intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" /> <sec:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp" default-target-url="/index.jsp"/> </sec:http> <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"></property> </bean> <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"> <property name="rolePrefix" value=""></property> </bean> <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter" /> ~~~ 在这里我们就不用自定义的AccessDecisionManager了,直接用Spring的AffirmativeBased,因为Spring本身提供的这些决策管理器就已经很强大了。 配置很简单,要想修改权限的前缀只需要修改roleVoter中的rolePrefix就可以了,如果不要前缀就让它为“”。 authenticatedVoter是为了支持IS_AUTHENTICATED这种认证,authenticatedVoter提供的3种认证,分别是 IS_AUTHENTICATED_ANONYMOUSLY 允许匿名用户进入 IS_AUTHENTICATED_FULLY 允许登录用户进入 IS_AUTHENTICATED_REMEMBERED 允许登录用户和rememberMe用户进入
';

(7)—- 解决UsernameNotFoundException无法被捕获的问题

最后更新于:2022-04-01 14:54:08

这个教程是我在往项目中一点一点添加 Spring Security的过程的一个笔记,也是我学习 Spring Security的一个过程。 在解决这个问题之前要先说一点authentication-provider默认加载的是DaoAuthenticationProvider类。 完成了上一章的内容后在测试的时候发现在UserDetailsService中抛出的UsernameNotFoundException无法被捕获。于是找到DaoAuthenticationProvider,源码看了好几遍没有看出端倪。然后直接查看最顶级的接口AuthenticationProvider。发现它只有一个方法如下 ~~~ Authentication authenticate(Authentication authentication) throws AuthenticationException; ~~~ 抛出AuthenticationException异常,而UsernameNotFoundException是AuthenticationException的子类,那问题应该就出在authenticate这个方法上了。 然后找到DaoAuthenticationProvider的父类AbstractUserDetailsAuthenticationProvider的authenticate方法,发现了这段代码。 ~~~ try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { logger.debug("User '" + username + "' not found"); if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } ~~~ 它这里有个hideUserNotFoundExceptions属性,默认是true。这样的话即便我们抛出了UsernameNotFoundException它也会转为BadCredentialsException,所以我们需要将hideUserNotFoundExceptions属性的值设为false,而在上一章中的那种配置方法是没有办法为其属性赋值的所以我们要手动注入.authentication-provider,所以配置就变成了下面的内容 ~~~ <sec:authentication-manager> <sec:authentication-provider ref="authenticationProvider" /> </sec:authentication-manager> <bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="hideUserNotFoundExceptions" value="false" /> <property name="userDetailsService" ref="userDetailService" /> <property name="userCache" ref="userCache" /> <property name="messageSource" ref="messageSource" /> <property name="passwordEncoder" ref="passwordEncode" /> <property name="saltSource" ref="saltSource" /> </bean> <!-- 配置密码加密类 --> <bean id="passwordEncode" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" /> <bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource"> <property name="userPropertyToUse" value="username"/> </bean> ~~~ 注意:如果在authentication-provider配置中用ref指定AuthenticationProvider则authentication-provider的子元素将都不可以用。 即下面的这种配置是错误的 ~~~ <sec:authentication-manager> <sec:authentication-provider ref="authenticationProvider" > <sec:password-encoder ref="passwordEncode"> <sec:salt-source user-property="username"/> </sec:password-encoder> </sec:authentication-provider> </sec:authentication-manager> ~~~ 所以我们的盐值加密就需要注入到AuthenticationProvider中了。 SaltSource是一个接口有两个实现类SystemWideSaltSource和ReflectionSaltSource。 SystemWideSaltSource :只能指定固定值 ReflectionSaltSource:可以指定UserDetails的属性,这里我们用的就是它 这样的话就可以保证在抛出UsernameNotFoundException时,前台能显示出来错误信息,如下所示。 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b2949f28.jpg) 在上一章中忘了介绍如何在前台显示登录是的异常信息,在这里补上。 UsernamePasswordAuthenticationFilter认证失败后,异常信息会写到Session中,key为SPRING_SECURITY_LAST_EXCEPTION 可以通过El表达式来获取到异常的信息。 ~~~ ${sessionScope.SPRING_SECURITY_LAST_EXCEPTION.message} ~~~
';

(6)—- 使用数据库管理用户及权限

最后更新于:2022-04-01 14:54:05

上一章已经把表结构上传了,今天这部分主要用到的表是 - SYS_USERS用户管理表 - SYS_ROLES角色管理表 - SYS_AUTHORITIES权限管理表 - SYS_USERS_ROLES用户角色表 - SYS_ROLES_AUTHORITIES角色权限表 要实现使用数据库管理用户,需要自定义用户登录功能,而Spring已经为我们提供了接口UserDetailsService ~~~ package org.springframework.security.core.userdetails; public interface UserDetailsService { /** * Locates the user based on the username. In the actual implementation, the search may possibly be case * insensitive, or case insensitive depending on how the implementation instance is configured. In this case, the * <code>UserDetails</code> object that comes back may have a username that is of a different case than what was * actually requested.. * * @param username the username identifying the user whose data is required. * * @return a fully populated user record (never <code>null</code>) * * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority */ UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } ~~~ UserDetailsService是一个接口,只有一个方法loadUserByUsername,根据方法名可以看出这个方法是根据用户名来获取用户信息,但是返回的是一个UserDetails对象。而UserDetails也是一个接口 ~~~ package org.springframework.security.core.userdetails; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import java.io.Serializable; import java.util.Collection; //这里省略了Spring的注释,只是我自己对这些方法的简单的注释,如果想了解Spring对这些方法的注释,请查看Spring源码 public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); //权限集合 String getPassword(); //密码 String getUsername(); //用户名 boolean isAccountNonExpired(); //账户没有过期 boolean isAccountNonLocked(); //账户没有被锁定 boolean isCredentialsNonExpired(); //证书没有过期 boolean isEnabled();//账户是否有效 } ~~~ 因此我们的SysUsers这个bean需要实现这个接口 ~~~ @Entity @DynamicUpdate(true) @DynamicInsert(true) @Table(name = "SYS_USERS", schema = "FYBJ") public class SysUsers implements UserDetails,Serializable { /** * */ private static final long serialVersionUID = -6498309642739707784L; // Fields private String userId; private String username; private String name; private String password; private Date dtCreate; private Date lastLogin; private Date deadline; private String loginIp; private String VQzjgid; private String VQzjgmc; private String depId; private String depName; private boolean enabled; private boolean accountNonExpired; private boolean accountNonLocked; private boolean credentialsNonExpired; @JsonIgnore private Set<SysUsersRoles> sysUsersRoleses = new HashSet<SysUsersRoles>(0); private Collection<GrantedAuthority> authorities; //.....省略setter,getter..... //如果属性是boolean(注:不是Boolean)类型的值,在生产getter时会变为isXxx,如enabled生产getter为isEnabled } ~~~ 这样写我们的SysUsers只要生产getter和setter方法就实现了UserDetails,同时还可以使用数据库来控制这些属性,两全其美。 在UserDetails中有个属性需要注意下Collection<GrantedAuthority>  authorities,这个属性中存储了这个用户所有的权限。 下面需要先写下SysUsers的DAO层,一个方法是根据用户名获取用户,一个方法是根据用户名获取用户所有的权限,这里我用的是Spring Data Jpa,如果不懂这个请自行从网上查阅资料 ~~~ public interface SysUsersRepository extends JpaRepository<SysUsers, String> { public SysUsers getByUsername(String username); public Collection<GrantedAuthority> loadUserAuthorities(String username); } ~~~ 其中getByUsername符合Spring的命名规范,所以这个方法不需要我们来实现,而loadUserAuthorities则需要我们自己动手实现 ~~~ public class SysUsersRepositoryImpl { protected Log logger = LogFactory.getLog(getClass()); @PersistenceContext private EntityManager entityManager; /** * 根据用户名获取到用户的权限并封装成GrantedAuthority集合 * @param username */ public Collection<GrantedAuthority> loadUserAuthorities(String username){ List<SysAuthorities> list = this.getSysAuthoritiesByUsername(username); List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>(); for (SysAuthorities authority : list) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(authority.getAuthorityMark()); auths.add(grantedAuthority); } return auths; } /** * 先根据用户名获取到SysAuthorities集合 * @param username * @return */ @SuppressWarnings("unchecked") private List<SysAuthorities> getSysAuthoritiesByUsername(String username){ String sql = "SELECT * FROM SYS_AUTHORITIES WHERE AUTHORITY_ID IN( "+ "SELECT DISTINCT AUTHORITY_ID FROM SYS_ROLES_AUTHORITIES S1 "+ "JOIN SYS_USERS_ROLES S2 ON S1.ROLE_ID = S2.ROLE_ID "+ "JOIN SYS_USERS S3 ON S3.USER_ID = S2.USER_ID AND S3.USERNAME=?1)"; Query query = this.entityManager.createNativeQuery(sql, SysAuthorities.class); query.setParameter(1, username); List<SysAuthorities> list = query.getResultList(); return list; } } ~~~ 不管是用Spring Data Jpa还是普通的方法只要实现这两个方法就可以了 最后也是最重要的一个类UserDetailsService ~~~ public class DefaultUserDetailsService implements UserDetailsService { protected final Log logger = LogFactory.getLog(getClass()); @Autowired private SysUsersRepository sysUsersRepository; @Autowired private MessageSource messageSource; @Autowired private UserCache userCache; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { Collection<GrantedAuthority> auths = new ArrayList<GrantedAuthority>(); SysUsers user = (SysUsers) this.userCache.getUserFromCache(username); if(user == null){ user = this.sysUsersRepository.getByUsername(username); if(user == null) throw new UsernameNotFoundException(this.messageSource.getMessage( "UserDetailsService.userNotFount", new Object[]{username}, null)); //得到用户的权限 auths = this.sysUsersRepository.loadUserAuthorities( username ); user.setAuthorities(auths); } logger.info("***********"+username+"*************"); logger.info(user.getAuthorities()); logger.info("****************************"); this.userCache.putUserInCache(user); return user; } } ~~~ 在loadUserByUsername方法中首先是从缓存中查找用户,如果找到用户就直接用缓存中的用户,如果没有找到就从数据库中获取用户信息。 从数据库中获取用户时先获取User对象,如果用户为空则抛出UsernameNotFoundException,其中UserDetailsService.userNotFount是在property文件中自定义的,如果获取到了user则再获取用户的权限,按照Spring的标准如果没有任何权限也是要抛出这个异常的,在这里我们就不做判断了。 登录后可以看到控制台打印出来以下信息 ~~~ ***********admin************* [AUTH_PASSWORD_MODIFY, AUTH_GG_FBGBGG, AUTH_GG_FBZNGG] **************************** ~~~ 说明我们登录成功并且已经获取到了权限,但是可能会出现如下页面 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b2932527.jpg) 这样就是你在数据库中存储的权限跟配置文件中的不对应,或者说访问资源是没有从用户的权限集合中找到这个权限。
';

(5)—- 国际化配置及UserCache

最后更新于:2022-04-01 14:54:03

这一章是为了给后面的讲解打基础的,主要介绍下国际化的配置及UserCache的配置及使用 国际化配置 ~~~ <!-- 定义上下文返回的消息的国际化 --> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="classpath:config/messages_zh_CN"/> </bean> ~~~ basename中配置的是消息文件的路径 在spring-security-core-3.2.0.M1.jar包中的org.springframework.security下可以找到国际化文件,可以直接拿来,这个类也可以用在项目中 ~~~ @Autowired private MessageSource messageSource; ~~~ 这样就可以在类中引如MessageSource使用了,MessageSource提供了下面三个方法 ~~~ String getMessage(String code, Object[] args, String defaultMessage, Locale locale); String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException; String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException; ~~~ 比如我们在property文件中定义了如下消息 ~~~ UserDetails.isLocked=用户已被锁定 UserDetails.userNotFound=用户{0}不存在 ~~~ 然后使用getMessage方法 getMessage("UserDetails.isLocked",null,null)  //用户已被锁定 getMessage("UserDetails.isLocked",new Object[]{“admin”},null)  //用户admin不存在 UserCache配置,通过ecahe实现 ~~~ <!-- 启用用户的缓存功能 --> <bean id="userCache" class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache"> <property name="cache" ref="userEhCache" /> </bean> <bean id="userEhCache" class="org.springframework.cache.ehcache.EhCacheFactoryBean"> <property name="cacheName" value="userCache" /> <property name="cacheManager" ref="cacheManager" /> </bean> <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" /> ~~~ ehcache.xml ~~~ <cache name="userCache" maxElementsInMemory="100" eternal="false" timeToIdleSeconds="600" timeToLiveSeconds="3600" overflowToDisk="true" /> ~~~ 注入ecache ~~~ @Autowired private UserCache userCache; ~~~ 这样在程序中就可以通过 this.userCache.getUserFromCache(username);获取到缓存中的用户对象 用户对象为UserDetails类型
';

(4)—- 数据库表结构的创建

最后更新于:2022-04-01 14:54:01

PD建模图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b290f663.jpg) 建模语句 ~~~ alter table SYS_AUTHORITIES_RESOURCES drop constraint FK_SYS_AUTH_REFERENCE_SYS_AUTH; alter table SYS_AUTHORITIES_RESOURCES drop constraint FK_SYS_AUTH_REFERENCE_SYS_RESO; alter table SYS_RESOURCES drop constraint FK_SYS_RESO_REFERENCE_SYS_MODU; alter table SYS_ROLES_AUTHORITIES drop constraint FK_SYS_ROLE_REFERENCE_SYS_ROLE; alter table SYS_ROLES_AUTHORITIES drop constraint FK_SYS_ROLE_REFERENCE_SYS_AUTH; alter table SYS_ROLES_MOUDLES drop constraint FK_SYS_ROLE_REFERENCE_SYS_MODU; alter table SYS_ROLES_MOUDLES drop constraint FK_S_ROLE_REFERENCE_SYS_ROLE; alter table SYS_USERS_ROLES drop constraint FK_SYS_USER_REFERENCE_SYS_USER; alter table SYS_USERS_ROLES drop constraint FK_SYS_USER_REFERENCE_SYS_ROLE; drop table PERSISTENT_LOGINS cascade constraints; drop table SYS_AUTHORITIES cascade constraints; drop table SYS_AUTHORITIES_RESOURCES cascade constraints; drop table SYS_MODULES cascade constraints; drop table SYS_RESOURCES cascade constraints; drop table SYS_ROLES cascade constraints; drop table SYS_ROLES_AUTHORITIES cascade constraints; drop table SYS_ROLES_MOUDLES cascade constraints; drop table SYS_USERS cascade constraints; drop table SYS_USERS_ROLES cascade constraints; /*==============================================================*/ /* Table: PERSISTENT_LOGINS */ /*==============================================================*/ create table PERSISTENT_LOGINS ( USERNAME VARCHAR2(64), SERIES VARCHAR2(64) not null, TOKEN VARCHAR2(64), LAST_USED TIMESTAMP, constraint PK_PERSISTENT_LOGINS primary key (SERIES) ); comment on table PERSISTENT_LOGINS is 'Spring Remember me 持久化'; /*==============================================================*/ /* Table: SYS_AUTHORITIES */ /*==============================================================*/ create table SYS_AUTHORITIES ( AUTHORITY_ID VARCHAR2(100) not null, AUTHORITY_MARK VARCHAR2(100), AUTHORITY_NAME VARCHAR2(100) not null, AUTHORITY_DESC VARCHAR2(200), MESSAGE VARCHAR2(100), ENABLE NUMBER, ISSYS NUMBER, MODULE_ID VARCHAR2(100), constraint PK_SYS_AUTHORITIES primary key (AUTHORITY_ID) ); /*==============================================================*/ /* Table: SYS_AUTHORITIES_RESOURCES */ /*==============================================================*/ create table SYS_AUTHORITIES_RESOURCES ( ID VARCHAR2(100) not null, RESOURCE_ID VARCHAR2(100) not null, AUTHORITY_ID VARCHAR2(100) not null, constraint PK_SYS_AUTHORITIES_RESOURCES primary key (ID) ); /*==============================================================*/ /* Table: SYS_MODULES */ /*==============================================================*/ create table SYS_MODULES ( MODULE_ID VARCHAR2(100) not null, MODULE_NAME VARCHAR2(100) not null, MODULE_DESC VARCHAR2(200), MODULE_TYPE VARCHAR2(100), PARENT VARCHAR2(100), MODULE_URL VARCHAR2(100), I_LEVEL NUMBER, LEAF NUMBER, APPLICATION VARCHAR2(100), CONTROLLER VARCHAR2(100), ENABLE NUMBER(1), PRIORITY NUMBER, constraint PK_SYS_MODULES primary key (MODULE_ID) ); comment on column SYS_MODULES.I_LEVEL is '1'; /*==============================================================*/ /* Table: SYS_RESOURCES */ /*==============================================================*/ create table SYS_RESOURCES ( RESOURCE_ID VARCHAR2(100) not null, RESOURCE_TYPE VARCHAR2(100), RESOURCE_NAME VARCHAR2(100), RESOURCE_DESC VARCHAR2(200), RESOURCE_PATH VARCHAR2(200), PRIORITY VARCHAR2(100), ENABLE NUMBER, ISSYS NUMBER, MODULE_ID VARCHAR2(100), constraint PK_SYS_RESOURCES primary key (RESOURCE_ID) ); comment on column SYS_RESOURCES.RESOURCE_TYPE is 'URL,METHOD'; /*==============================================================*/ /* Table: SYS_ROLES */ /*==============================================================*/ create table SYS_ROLES ( ROLE_ID VARCHAR2(100) not null, ROLE_NAME VARCHAR2(100), ROLE_DESC VARCHAR2(200), ENABLE NUMBER, ISSYS NUMBER, MODULE_ID VARCHAR2(100), constraint PK_SYS_ROLES primary key (ROLE_ID) ); /*==============================================================*/ /* Table: SYS_ROLES_AUTHORITIES */ /*==============================================================*/ create table SYS_ROLES_AUTHORITIES ( ID VARCHAR2(100) not null, AUTHORITY_ID VARCHAR2(100) not null, ROLE_ID VARCHAR2(100) not null, constraint PK_SYS_ROLES_AUTHORITIES primary key (ID) ); /*==============================================================*/ /* Table: SYS_ROLES_MOUDLES */ /*==============================================================*/ create table SYS_ROLES_MOUDLES ( ID VARCHAR2(100) not null, MODULE_ID VARCHAR2(100) not null, ROLE_ID VARCHAR2(100) not null, constraint PK_SYS_ROLES_MOUDLES primary key (ID) ); comment on table SYS_ROLES_MOUDLES is '控制角色对模块的访问权,主要用于生成菜单'; /*==============================================================*/ /* Table: SYS_USERS */ /*==============================================================*/ create table SYS_USERS ( USER_ID VARCHAR2(100) not null, USERNAME VARCHAR2(100) not null, NAME VARCHAR2(100), PASSWORD VARCHAR2(100) not null, DT_CREATE DATE default SYSDATE, LAST_LOGIN DATE, DEADLINE DATE, LOGIN_IP VARCHAR2(100), V_QZJGID VARCHAR2(100), V_QZJGMC VARCHAR2(100), DEP_ID VARCHAR2(100), DEP_NAME VARCHAR2(100), ENABLED NUMBER, ACCOUNT_NON_EXPIRED NUMBER, ACCOUNT_NON_LOCKED NUMBER, CREDENTIALS_NON_EXPIRED NUMBER, constraint PK_SYS_USERS primary key (USER_ID) ); /*==============================================================*/ /* Table: SYS_USERS_ROLES */ /*==============================================================*/ create table SYS_USERS_ROLES ( ID VARCHAR2(100) not null, ROLE_ID VARCHAR2(100) not null, USER_ID VARCHAR2(100) not null, constraint PK_SYS_USERS_ROLES primary key (ID) ); alter table SYS_AUTHORITIES_RESOURCES add constraint FK_SYS_AUTH_REFERENCE_SYS_AUTH foreign key (AUTHORITY_ID) references SYS_AUTHORITIES (AUTHORITY_ID); alter table SYS_AUTHORITIES_RESOURCES add constraint FK_SYS_AUTH_REFERENCE_SYS_RESO foreign key (RESOURCE_ID) references SYS_RESOURCES (RESOURCE_ID); alter table SYS_RESOURCES add constraint FK_SYS_RESO_REFERENCE_SYS_MODU foreign key (MODULE_ID) references SYS_MODULES (MODULE_ID); alter table SYS_ROLES_AUTHORITIES add constraint FK_SYS_ROLE_REFERENCE_SYS_ROLE foreign key (ROLE_ID) references SYS_ROLES (ROLE_ID); alter table SYS_ROLES_AUTHORITIES add constraint FK_SYS_ROLE_REFERENCE_SYS_AUTH foreign key (AUTHORITY_ID) references SYS_AUTHORITIES (AUTHORITY_ID); alter table SYS_ROLES_MOUDLES add constraint FK_SYS_ROLE_REFERENCE_SYS_MODU foreign key (MODULE_ID) references SYS_MODULES (MODULE_ID); alter table SYS_ROLES_MOUDLES add constraint FK_S_ROLE_REFERENCE_SYS_ROLE foreign key (ROLE_ID) references SYS_ROLES (ROLE_ID); alter table SYS_USERS_ROLES add constraint FK_SYS_USER_REFERENCE_SYS_USER foreign key (USER_ID) references SYS_USERS (USER_ID); alter table SYS_USERS_ROLES add constraint FK_SYS_USER_REFERENCE_SYS_ROLE foreign key (ROLE_ID) references SYS_ROLES (ROLE_ID); ~~~ 这些都是在后面讲解的过程中要用到的表 [PD建模下载](http://download.csdn.net/detail/jaune161/6854357)
';

(3)—- 自定义登录页面

最后更新于:2022-04-01 14:53:59

在项目中我们肯定不能使用Spring自己生成的登录页面,而要用我们自己的登录页面,下面讲一下如何自定义登录页面,先看下配置 ~~~ <sec:http auto-config="true"> <sec:intercept-url pattern="/app.jsp" access="ROLE_SERVICE"/> <sec:intercept-url pattern="/**" access="ROLE_ADMIN"/> <sec:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp" default-target-url="/index.jsp"/> </sec:http> ~~~ 使用form-login配置来指定我们自己的配置文件,其中 login-page:登录页面 authentication-failure-url:登录失败后跳转的页面 default-target-url:登录成功后跳转的页面 在登录页面中 表单提交地址为:j_spring_security_check 用户名的name为:j_username 密码的name为:j_password 提交方式为POST 重启Tomcat后,再次打开项目发现登录页面已经变成了我们自己的登录页面,如下图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b28d6d98.jpg) 如果提示页面循环的错误,是因为没有设置登录页面不需要验证,增加如下配置就可以了 ~~~ <security:http pattern="/login.jsp" security="none" /> ~~~ 输入用户名密码后跳转到了我们指定的页面 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b28a8e8a.jpg) 注:重启Tomcat有时候并不会使Session失效,在测试的时候可能会出现,明明重启了Tomcat可以访问资源时却没有跳到登录页面。所以需要重启浏览器再重试就可以了。
';

(2)—-SpringSecurity简单测试

最后更新于:2022-04-01 14:53:56

前面讲到了SpringSecurity的简单配置,今天做一个简单的测试,先看配置文件 ~~~ <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:sec="http://www.springframework.org/schema/security" 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 http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <!-- 配置不过滤的资源(静态资源及登录相关) --> <sec:http pattern="/**/*.css" security="none"></sec:http> <sec:http pattern="/**/*.jpg" security="none"></sec:http> <sec:http pattern="/**/*.jpeg" security="none"></sec:http> <sec:http pattern="/**/*.gif" security="none"></sec:http> <sec:http pattern="/**/*.png" security="none"></sec:http> <sec:http pattern="/js/*.js" security="none"></sec:http> <sec:http pattern="/login.jsp" security="none"></sec:http> <sec:http pattern="/getCode" security="none" /><!-- 不过滤验证码 --> <sec:http pattern="/test/**" security="none"></sec:http><!-- 不过滤测试内容 --> <sec:http auto-config="true"> <sec:intercept-url pattern="/app.jsp" access="ROLE_SERVICE"/> <sec:intercept-url pattern="/**" access="ROLE_ADMIN"/> </sec:http> <sec:authentication-manager> <sec:authentication-provider> <sec:user-service > <sec:user name="admin" password="admin" authorities="ROLE_ADMIN"/> </sec:user-service> </sec:authentication-provider> </sec:authentication-manager> </beans> ~~~ 其中 <sec:http pattern="" security="none"></sec:http> 是忽略拦截某些资源的意思,主要是针对静态资源 <sec:intercept-url pattern="/app.jsp" access="ROLE_SERVICE"/> 表示访问app.jsp时,需要ROLE_SERVICE权限 <sec:intercept-url pattern="/**" access="ROLE_ADMIN"/> 表示访问任何资源都需要ROLE_ADMIN权限。 注:/**的配置要放到最后,因为如果放到最前面的话就失去了拦截意义,任何只要有ROLE_ADMIN权限的用户都可以访问任何资源,并不会对app.jsp拦截。因为在访问app.jsp的时候先经过<sec:intercept-url pattern="/**" access="ROLE_ADMIN"/>,、/**又表示任何资源,所以只要具有ROLE_ADMIN权限就会放行。如果放到最后,先经过<sec:intercept-url pattern="/app.jsp" access="ROLE_SERVICE"/>,这时候访问app.jsp是就会先判断用户是否有ROLE_SERVICE权限,如果有则放行,反之拦截。 权限必须已ROLE_开头,在后面的文章中我们会讲到如何修改权限头和去掉权限头 authentication-manager用来配置用户管理,在这里我们定义了一个admin用户并且具有ROLE_ADMIN权限,也就是说我们访问任何资源都可以但是访问app.jsp时将被拦截 在没有自定义登录页面之前,SpringSecurity会自动生成登录页面,如下图 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b2894562.jpg) 然后输入admin/admin登录 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b28a8e8a.jpg) 然后访问app.jsp发现已被spring拦截,说明我们的配置成功了 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-22_576a4b28bfafa.jpg) 符:在JSP页面获取当前登录的用户名的方法 首先引入taglib ~~~ <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> ~~~ 然后在jsp页面中使用下面的方法就可以获取到用户名了 ~~~ <sec:authentication property="name"/> ~~~ 在此仅仅是为了方便测试所以只说下获取用户名的方法,如何获取用户的其他信息将在后续的章节中讲到
';

(1)—-SpringSecurity3.2环境搭建

最后更新于:2022-04-01 14:53:54

目前Spring官方只提供Meven的下载方式。但在[http://maven.springframework.org](http://maven.springframework.org/)中有SpringSecurity及其他所有Spring产品的下载方式。 [http://maven.springframework.org/release/org/springframework/](http://maven.springframework.org/release/org/springframework/)中有Spring相关的所有下载,但好像直到3.2版的,最新的版本在这个里面找不到 [](http://maven.springframework.org/release/org/springframework/security/spring-security-web/3.2.0.RELEASE/)[http://maven.springframework.org/release/org/springframework/security/spring-security/3.2.0.RELEASE/](http://maven.springframework.org/release/org/springframework/security/spring-security/3.2.0.RELEASE/)这个是SpringSecurity3.2的下载地址 Meven下载地址: ~~~ <dependencies> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>3.2.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>3.2.0.RELEASE</version> </dependency> </dependencies> ~~~ 本教程是基于SpringMVC3.2+Hibernate4+JPA2.0+SpringSecurity3.2的环境。SpringMVC3.2+Hibernate4+JPA2.0环境的搭建在这里就不多说了,主要讲下SpringSecurity的环境搭建 web.xml配置 ~~~ <!-- 加载Spring的配置文件 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:applicationContext.xml, classpath:applicationContext-security.xml </param-value> </context-param> <!-- SpringSecurity 核心过滤器配置 --> <filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ~~~ applicationContext-security.xml命名空间配置,官方提供了两种配置方案 第一种、命名空间用beans开头,但是在配置中一直需要用<security:*>来配置。 ~~~ <beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" 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 http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> ... </beans> ~~~ 第二种、命名空间用security开头,在配置中不需要security前缀,但是bean的配置需要用<beans:bean>配置。 ~~~ <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="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 http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> ... </beans:beans> ~~~ 到此为止SpringSecurity的环境配置已基本完成 命名空间的配置可在spring的官方文档,第4章 Security Namespace Configuration 中找到,一下附上链接地址 [http://docs.spring.io/spring-security/site/docs/3.2.0.RELEASE/reference/htmlsingle/#ns-config](http://docs.spring.io/spring-security/site/docs/3.2.0.RELEASE/reference/htmlsingle/#ns-config)
';

(大纲)—-学习过程分享

最后更新于:2022-04-01 14:53:52

今天给大家分享一下我学习SpringSecurity的过程,及我随后要讲的Spring Security的学习大纲。一为指导想学习SpringSecurity的同学,二为留着自己备用,以便忘了的时候翻出来看看。第一次写博客,写的不好的地方还请大家勿喷。 Spring Security是Spring的一个安全框架,它的前身是Acegi Security.这个框架主要分为两个部分,认证、验证。 - 认证是为用户建立一个他所声明的主体,主体一般是指用户。 - 认证是判断用户访问的资源是否有足够的权限。 因为项目的需求,需要用到细粒度的权限控制,所以决定用SpringSecurity来做。也因为我自己一直想研究SpringSecurity,所以项目中权限的部分就由我来做了。我学习SpringSecurity大概用了两个星期,虽然研究的不够深,但在项目中用也够了。下面我分享下我学习SpringSecurity的过程: - SpringSecurity环境搭建(我用的是SpringSecurity  3.2) - SpringSecurity简单测试 - 自定义登录页面 - 使用数据库进行用户认证、密码加密 - 使用数据库进行URL资源认证 - 使用数据库进行Method资源认证 - remember-me功能、验证码功能的实现 教程大纲安排如下 1. [SpringSecurity环境搭建(与SpringMVC整合)](http://blog.csdn.net/jaune161/article/details/17640071) 1. [SpringSecurity简单测试](http://blog.csdn.net/jaune161/article/details/18350183) 1. [自定义登录页面及](http://blog.csdn.net/jaune161/article/details/18351247) 1. [数据库表结构创建](http://blog.csdn.net/jaune161/article/details/18353397) 1. [国际化配置及UserCach](http://blog.csdn.net/jaune161/article/details/18356061) 1. [使用数据库管理用户及权限](http://blog.csdn.net/jaune161/article/details/18354599) 1. [解决UsernameNotFoundException无法被捕获的问题](http://blog.csdn.net/jaune161/article/details/18359321) 1. [自定义决策管理器及修改权限前缀](http://blog.csdn.net/jaune161/article/details/18401233) 1. [自定义AccessDeniedHandler](http://blog.csdn.net/jaune161/article/details/18403113) 1. [自定义登录成功后的处理程序及修改默认验证地址](http://blog.csdn.net/jaune161/article/details/18445003) 1. [使用数据库来管理资源](http://blog.csdn.net/jaune161/article/details/18446481) 1. [使用数据库来管理方法](http://blog.csdn.net/jaune161/article/details/18453785) 1. [验证码功能的实现](http://blog.csdn.net/jaune161/article/details/18502265) 1. [Logout和SessionManager](http://blog.csdn.net/jaune161/article/details/18736687) SpringSecurity参考资料  [http://www.mossle.com/docs/auth/html/index.html](http://www.mossle.com/docs/auth/html/index.html)  [http://www.blogjava.net/SpartaYew/archive/2011/06/15/350630.html](http://www.blogjava.net/SpartaYew/archive/2011/06/15/350630.html) 源码下载地址 [http://download.csdn.net/detail/jaune161/7415961](http://download.csdn.net/detail/jaune161/7415961) 只有SpringSecurity的源码和配置文件,并不是一个完整的可以运行的项目,所以不能直接运行,仅能作为参考。项目涉及到公司的东西所以不能上传,望体谅!
';

前言

最后更新于:2022-04-01 14:53:50

> 原文出处:[SpringSecurity教程](http://blog.csdn.net/column/details/springsecurity.html) 作者:[王成委](http://blog.csdn.net/jaune161) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # SpringSecurity教程 > SpringSecurity从入门到使用,精通就要靠自己了 哈哈
';