【JavaEE】SSH+Spring Security自定义Security的部分处理策略
本文建立在 SSH与Spring Security整合 一文的基础上,从这篇文章的example上做修改,或者从 配置了AOP 的example上做修改皆可。这里主要补充我在实际使用Spring Security中常用的一些前文最基本example中没能提供的功能,主要包括自定义403错误页面、自定义认证管理器的内容提供者、自定义登录成功的回调接口,自定义json访问时未登录和403错误的返回内容和用代码模拟Spring Security的验证。
在搭建Spring Security的时候,http标签内配置了这样的子标签:
<form-login login-page="/" default-target-url="/" authentication-failure-url="/?login=error" />
这个属性是说,如果待访问的资源需要一定的权限,但是当前用户没有登录,那么应该跳转到login-page上去登录,如果登录成功了,就跳转到default-target-url上去,如果登录失败了,就跳转到anthentication-failure-url上去,但是缺一个配置,那就是如果我登录了,并且是USER权限,现在访问了一个需要ADMIN权限的资源,那么怎么办?实际中会返回一个默认的界面:
那么这个界面太丑了,怎么自定义,这个非常简单,只需要在http标签中加入下面的一个:
<access-denied-handler error-page="/denied"/>
也就是说,如果访问权限不够,就会访问/denied这个资源,因为Springmvc会拦截所有的请求,这个也不例外,在HomeController中加入:
@RequestMapping("/denied") public String denied(){ return "denied"; }
在webapps/pages目录下创建denied.jsp:
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <c:set var="base" value="${pageContext.request.contextPath }/" scope="session"/> <html> <body> <h2>您的访问权限不够!!</h2> <h3>3秒钟之后跳转到首页。。。或点击<a href="${base }">首页</a></h3> </body> <script type="text/javascript"> setTimeout(function(){ location.href = "${base }"; }, 3000); </script> </html>
再次访问受限的资源就会跳转到这个界面上。
先回顾一下前文中怎么做用户名密码验证的:
<authentication-manager> <authentication-provider> <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select username, password, 1 from user where username = ?" authorities-by-username-query="select u.username, r.role from user u left join role r on u.role_id=r.id where username = ?" /> </authentication-provider> </authentication-manager>
指定数据源,根据用户提交上来的用户名发两条sql语句,获取到password和role,然后拿password和用户提交的密码(根据配置可能会做加盐的处理)匹配,如果登录成功,该用户的信息就以role所代表的权限保存了起来,但是有时候,对用户名密码的获取,不能够通过简单的两条sql语句来获取,那又应该怎么办呢?这就需要我们来自定义了,基本思路是我们写一个bean,Spring把用户名给这个bean,这个bean自己去找密码权限应该是什么,最后封装成一个User对象返回给Spring,也就是说,我们需要写自己的jdbc-user-service。下面就来实现它,创建一个package叫做security,再写一个类EssentialUser,并实现Spring的UserDetails接口,这个类就是Spring最终需要的User对象:
package org.zhangfc.demo4ssh.security; import ......; public class EssentialUser implements UserDetails { private static final long serialVersionUID = -3369448632273314162L; private int id; private String role; private String username; private String password; public EssentialUser(User user) { this.id = user.getId(); this.role = user.getRole().getRole(); this.username = user.getUsername(); this.password = user.getPassword(); } // setter and getter of id, role // setter of username, password @Override public String getUsername() { return username; } @Override public String getPassword() { return password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> auths = new ArrayList<>(); auths.add(new SimpleGrantedAuthority(this.role)); return auths; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
下面需要写一个Service来把这个对象给Spring,还是在security包下面创建MyUserDetailsService,实现Spring的UserDetailsService接口,我这儿简单起见了,我就只new了一个User对象,实际上应该是查询好了必要信息的对象:
public class MyUserDetailsService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User u = new User(); // 根据username来得到User对象 EssentialUser eu = new EssentialUser(u); return eu; } }
剩下的就很简单了,注册一下这个bean,并把它作为认证信息的提供者:
<beans:bean id="userDetails" class="cn.edu.tju.ina.estuary.security.MyUserDetailsService" /> <authentication-manager alias="authenticationManager"> <authentication-provider user-service-ref="userDetails" /> </authentication-manager>
有时候,Spring存的那个UserDetails用户信息不全,而且因为是Spring的接口,有时候用起来也不方便,我们希望在登录成功之后再在session中存一份当前用户对象,登录成功之后Spring会跳转到配置的URL上,但是很多时候,登录成功就是跳回首页,访问首页没必要再分是不是刚登录,所以要是Spring Security有登录之后的回调接口,存session的工作就可以在那里做了,这个想法当然是可行的。在security这个package下创建类AfterAuthSuccess,继承SimpleUrlAuthenticationSuccessHandler:
public class AfterAuthSuccess extends SimpleUrlAuthenticationSuccessHandler { @Autowired private UserService userService; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { RequestCache requestCache = new HttpSessionRequestCache(); SavedRequest savedRequest = requestCache.getRequest(request, response); HttpSession session = request.getSession(); SecurityContext sc = SecurityContextHolder.getContext(); String userName = sc.getAuthentication().getName(); User u = userService.findByUsername(userName); session.setAttribute("currentUser", u); if (savedRequest == null) { // if click login to open login page, savedRequest will be null. super.onAuthenticationSuccess(request, response, authentication); return; } clearAuthenticationAttributes(request); String targetUrl = savedRequest.getRedirectUrl(); if(targetUrl != null && "".equals(targetUrl)){ super.onAuthenticationSuccess(request, response, authentication); return; } getRedirectStrategy().sendRedirect(request, response, targetUrl); } }
这段代码可以直接用,看上去很复杂,是因为考虑了一些情况,比如访问A页面发现没有登录,这时候会跳转到登录页面去登录,登录成功之后会直接跳到A上去。
然后在xml文件中配置一下这个bean:
<http auto-config="true"> <intercept-url pattern="/admin**" access="ROLE_ADMIN" /> <form-login login-page="/" authentication-success-handler-ref="authSuccess" default-target-url="/" authentication-failure-url="/?login=error" /> <access-denied-handler error-page="/denied"/> <logout logout-success-url="/" /> </http> <beans:bean id="authSuccess" class="org.zhangfc.demo4ssh.security.AfterAuthSuccess" />
web应用中有很多接口可能是为移动端设计的,移动端有自己的权限控制方案,或者web也可能频繁请求json资源,那么对这些接口,未登录的时候就不能再跳转到login-page,权限不够的时候也不能再返回个403页面,这就需要自己来配置,原来有一个http标签,用来处理所有的请求,现在在它前面加一个,只处理/json开头的地址:
<http pattern="/json**" entry-point-ref="jsonEntryPoint"> <intercept-url pattern="/json**" access="ROLE_USER" /> <access-denied-handler error-page="/900" /> </http>
只有ROLE_USER权限是可以访问这些资源的(ROLE_ADMIN也不行),如果是权限不够呢,跳转到/900,如果是未登录,也就是Spring Security没有存这个票据,那么Spring会扔出一个异常,扔到ExceptionTranslationFilter链里去,EntryPoint就是来处理这个问题的,来看这个引用的bean:
<beans:bean id="jsonEntryPoint" class="org.zhangfc.demo4ssh.security.JsonEntryPoint"> <beans:property name="url" value="/901"></beans:property> </beans:bean>
这儿指定当未登录的时候请求/901。看看这个bean怎么来实现:
public class JsonEntryPoint implements AuthenticationEntryPoint { private String url = "/"; @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { request.getRequestDispatcher(url).include(request, response); } public void setUrl(String url) { this.url = url; } }
非常简单,这个AuthenticationException还可以拿来做一些更细致的判断,不过我没有去做太多尝试。
最后只要在/900和/901的Controller里面返回对应的json串就可以了。
前面的介绍都是Spring自己去校验用户名密码之后就登录了,有时候我们需要模拟Spring登录,比如注册之后直接变成登录状态,当然也可以用代码发一个登录请求,不过有些麻烦,不如直接用代码来登录,其实也很简单:
@Autowired @Qualifier("authenticationManager") protected AuthenticationManager authenticationManager; private void setAuthInSpringSecuity(String username, String password, HttpServletRequest request) { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( username, password); try { token.setDetails(new WebAuthenticationDetails(request)); Authentication authenticatedUser = authenticationManager .authenticate(token); SecurityContextHolder.getContext().setAuthentication( authenticatedUser); request.getSession() .setAttribute( HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext()); } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } }
这儿就只把代码贴在这里了,没有什么需要解释的,Spring验证登录成功之后会把当前用户对象放到session里,最后几行做的就是这个事情。