以前也没有接触过Spring Security,最近公司要重构一个安全登录控制,顺便学习了一下此框架,我会将我在学习过程中遇到的疑惑一一告诉大家,希望对大家有一些帮助,废话不多说直接上代码才是大家最关心的:
1.第一步得搭建Spring Security环境噻;
我使用的开发环境是IDEA:
springSecurity='3.2.9.RELEASE' springMobile='1.1.5.RELEASE'
"org.springframework.security:spring-security-web:$springSecurity", "org.springframework.security:spring-security-config:$springSecurity", "org.springframework.security:spring-security-remoting:$springSecurity", "org.springframework.security:spring-security-acl:$springSecurity", "org.springframework.security:spring-security-aspects:$springSecurity", "org.springframework.security:spring-security-crypto:$springSecurity", "org.springframework.security:spring-security-ldap:$springSecurity", "org.springframework.security:spring-security-taglibs:$springSecurity", "org.springframework.mobile:spring-mobile-device:$springMobile",当然也许要Spring的基础jar,这基础环境就大家自己去搭建了
2.编写Spring-security.xml文件,这个是关键,直接上代码
xml version="1.0" encoding="UTF-8"?>3.web.xml配置xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.2.xsd"> <security:http entry-point-ref="loginUrlAuthenticationEntryPoint" use-expressions="true"> <security:intercept-url pattern="/succ/*" access="hasRole('ROLE_MOBILE_CUSTOMER')"/> <security:session-management session-fixation-protection="newSession"/> <security:custom-filter ref="simpleAuthenticationFilter" position="PRE_AUTH_FILTER" /> security:http> id="loginUrlAuthenticationEntryPoint" class="com.ct10000.sc.sctelwap.wap.sso.entrypoint.WAPLoginUrlAuthenticationEntryPoint"> index="0" value="/succ/ssosuccessed"/> id="simpleAuthenticationFilter" class="com.ct10000.sc.sctelwap.wap.sso.filter.SimpleAuthenticationFilter"> name="authenticationManager" ref="authenticationManager"/> name="authenticationSuccessHandler" ref="authenticationSuccessHandler"/> name="authenticationFailureHandler" ref="authenticationFailureHandler"/> name="authenticationDetailsSource" ref="authenticationDetailsSource"/> id="authenticationSuccessHandler" class="com.ct10000.sc.sctelwap.wap.sso.handler.WAPAuthenticationSuccessHandler"/> id="authenticationFailureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> name="defaultFailureUrl" value="/error/ssofailed"/> id="simpleAuthenticationProvider" class="com.ct10000.sc.sctelwap.wap.sso.provider.SimpleAuthenticationProvider"/> id="authenticationDetailsSource" class="com.ct10000.sc.sctelwap.wap.sso.source.WAPAuthenticationDetailsSource"/> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="simpleAuthenticationProvider"/> security:authentication-manager>
4此处贴上Spring security的运行机制;deviceResolverRequestFilter org.springframework.mobile.device.DeviceResolverRequestFilter deviceResolverRequestFilter /* springSecurityFilterChain org.springframework.web.filter.DelegatingFilterProxy springSecurityFilterChain /*
SimpleAuthenticationProvider细节验证:
执行SimpleAuthenticationUserDetailsService 方法查询客户资料
loadUserDetails()中执行逻辑:
1.POST请求认证接口,返回数据包括用户号码,号码类型,渠道ID;
2.根据用户号码,号码类型,渠道ID查询客户资料,返回MobileUser;
验证成功之后Spring Security会自动把用户信息保存到上下文中,获取用户信息
user = (MobileUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
5.关键代码来了
Filter:
/** * Created by leitao on 2016/12/21. * 单点登陆过滤器 */ public class SimpleAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * 日志对象 */ private Logger log = LogManager.getLogger(getClass()); /** * 单点登陆过滤url */ public static final String SSO_AUTHENTICATION_FILTER_PROCESSES_URL = "/ssologin/index.html"; /** * 如果参数传递渠道为空,则默认此渠道 */ public static final String DEFAULT_CHANNEL = "xyzd"; /** * 创建新的实例 */ public SimpleAuthenticationFilter() { this(SSO_AUTHENTICATION_FILTER_PROCESSES_URL); } protected SimpleAuthenticationFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { log.info("进入SimpleAuthenticationFilter"); //参数验证 String params = request.getParameter("params");//入参 String channel = request.getParameter("channel");//渠道 log.info("入参params:" + params + ",渠道channel:" + channel); UsernamePasswordAuthenticationToken token = null; Authentication authentication=null; if (StringUtils.isEmpty(params)) { redirectRequest(request, response); }else { //判断渠道是否为空 if (StringUtils.isEmpty(channel)) { channel = DEFAULT_CHANNEL; } token = new UsernamePasswordAuthenticationToken(params, channel); //设置其他认证信息 setDetail(request, token); //进入provider处理链,进行用户信息详细认证 authentication = this.getAuthenticationManager().authenticate(token); //验证之后的用户对象 MobileUser mobiuser= (MobileUser) authentication.getPrincipal(); //跳转到要单点的url String url = mobiuser.getUrl(); if(!StringUtils.isEmpty(url)){ log.info("将单点跳转地址存入session:"+url); redirectUrl(request,url); } } return authentication; } /** * 登录参数错误重定向请求 * * @param request 请求对象 * @param response 响应对象 * @throws IOException */ private void redirectRequest(HttpServletRequest request, HttpServletResponse response) throws IOException { RedirectUrlBuilder builder = new RedirectUrlBuilder(); builder.setScheme("http"); builder.setServerName(request.getServerName()); builder.setPort(request.getServerPort()); builder.setContextPath(request.getContextPath()); builder.setPathInfo("/login/error"); log.info("request url:" + builder.getUrl()); response.sendRedirect(builder.getUrl()); } /** * 将要单点的地址存入session * @param request * @param url */ private void redirectUrl(HttpServletRequest request, String url) { RedirectUrlBuilder builder = new RedirectUrlBuilder(); builder.setScheme("http"); builder.setServerName(request.getServerName()); builder.setPort(request.getServerPort()); builder.setContextPath(request.getContextPath()); builder.setPathInfo(url); log.info("request url:" + builder.getUrl()); //将需要跳转的url存入session,SpringSecurity验证成功后SuccessHandler从Session中取出url跳转 request.getSession().setAttribute("returnUrl", builder.getUrl()); } /** * 设置额外的认证信息,如ip,设备来源...等 * * @param request * @param authRequest */ private void setDetail(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } }Provider:
/** * Created by leitao on 2016/12/21. * SSO认证. */ public class SimpleAuthenticationProvider implements AuthenticationProvider, InitializingBean, Ordered { /** * 日志对象 */ private Logger log = LogManager.getLogger(getClass()); @Autowired private AuthenticationUserDetailsServicesimpleAuthenticationUserDetailsService; private int order = -1; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { log.info("进入SimpleAuthenticationProvider:"+authentication); if (!supports(authentication.getClass())){ return null; } if (!(authentication.getPrincipal() instanceof String)){ return null; } //加载用户详细信息 UserDetails userDetails = simpleAuthenticationUserDetailsService.loadUserDetails((UsernamePasswordAuthenticationToken) authentication); log.info("userDetails:"+userDetails); UsernamePasswordAuthenticationToken resultToken = new UsernamePasswordAuthenticationToken(userDetails,userDetails.getPassword(),userDetails.getAuthorities()); resultToken.setDetails(authentication.getDetails()); return resultToken; } /** * 判断传入的对象是否是UsernamePasswordAuthenticationToken,是就执行authenticate方法,不是就执行下一个Provider * @param authentication * @return */ @Override public boolean supports(Class> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(simpleAuthenticationUserDetailsService, "An AuthenticationUserDetailsService must be set"); } @Override public int getOrder() { return order; } }
SimpleAuthenticationUserDetailsService:
/** * Created by leitao on 2016/12/21. * SSO用户信息认证 */ @Service public class SimpleAuthenticationUserDetailsService implements AuthenticationUserDetailsService{ /** * 日志对象 */ Logger log = LogManager.getLogger(getClass()); /** * SSO 请求认证 */ @Autowired private ISimpleAuthService simpleAuthService; /** * SSO 日志 */ @Autowired private ISsoLogService ssoLogService; @Override public UserDetails loadUserDetails(UsernamePasswordAuthenticationToken token) throws UsernameNotFoundException { log.info("进入SimpleAuthenticationUserDetailsService:"+token); MobileUser user = null; try { String channelAlias = token.getCredentials().toString(); log.info("渠道别名channelAlias:"+channelAlias); //单点认证 Map ,String> map =simpleAuthService.verificationRequest(channelAlias); if (null ==map || map.size()==0){ log.info("单点请求认证失败,可能没有此渠道"); throw new VerificationRequestErrorException(); } String number = map.get("Number");//电话号码 String acrType = map.get("NumberType");//号码类型 String code = map.get("Citycode");//区号 String channel_id = map.get("ChannelID");//渠道ID String url = map.get("Url");//单点访问地址 log.info("单点认证出参电话号码Number:"+number+",号码类型NumberType:"+acrType+",区号Citycode:"+code+",单点访问地址:"+url); //通过登录号码从CRM查询用户注册信息,acrType 号码类型(9手机,固话1,宽带2) log.info("根据号码从CRM查询客户资料."); CustInfo custInfo = CrmTool.qryCustInfo(number, acrType, code); //如果没有查到相应的用户信息则抛此异常 if (null == custInfo){ log.info("没有找到此用户:"+number); throw new RegisterDataNotFoundException(); }else { log.info("用户姓名:" + custInfo.getCustName()); CustomerDetail customerDetail = new CustomerDetail(); customerDetail.setCustInfo(custInfo); user = new MobileUser(number, token.getCredentials().toString(), MobileUserAuthority.getAuthorities(customerDetail), customerDetail, channel_id,url); ssoLogService.saveSsoLog(number,acrType,code,channel_id,url,((WAPAuthenticationDetails) token.getDetails()).getRemoteAddress()); } }catch (VerificationRequestErrorException e){ throw new UsernameNotFoundException(e.getMessage(), e.getCause()); }catch (RegisterDataNotFoundException e){ throw new UsernameNotFoundException(e.getMessage(), e.getCause()); } return user; } }
ssoLogService
@Override public Map, String> verificationRequest(String channelAlias) { Map ,String> outputParamMap = null; String endpointAddress=""; //根据渠道别名查询渠道信息 List
WAPAuthenticationDetails
/** * Created by leitao on 2016/12/22. * WAP认证细节对象 */ public class WAPAuthenticationDetails implements Serializable { private static final int HASH_CODE = 7654; private static final int SEVEN = 7; private String remoteAddress; private String sessionId; /** * 设备来源 */ private DeviceOrigin deviceOrigin; /** * * @return IP地址 */ public String getRemoteAddress() { return remoteAddress; } public void setRemoteAddress(String remoteAddress) { this.remoteAddress = remoteAddress; } public String getSessionId() { return sessionId; } public void setSessionId(String sessionId) { this.sessionId = sessionId; } public DeviceOrigin getDeviceOrigin() { return deviceOrigin; } public void setDeviceOrigin(DeviceOrigin deviceOrigin) { this.deviceOrigin = deviceOrigin; } public WAPAuthenticationDetails(){ } /** * 创建新实例 * @param request */ public WAPAuthenticationDetails(HttpServletRequest request) { HttpSession session = request.getSession(false); this.sessionId = (session != null) ? session.getId() : null; obtainRemoteAddress(request); obtainDeviceOrigin(request); } private void obtainRemoteAddress(HttpServletRequest request) { remoteAddress = request.getHeader("x-forwarded-for"); if (remoteAddress == null || remoteAddress.length() == 0 || "unknown".equalsIgnoreCase(remoteAddress)) { remoteAddress = request.getHeader("Proxy-Client-IP"); } if (remoteAddress == null || remoteAddress.length() == 0 || "unknown".equalsIgnoreCase(remoteAddress)) { remoteAddress = request.getHeader("WL-Proxy-Client-IP"); } if (remoteAddress == null || remoteAddress.length() == 0 || "unknown".equalsIgnoreCase(remoteAddress)) { remoteAddress = IPGetTool.getIpAddr(request); } } /** * 获取设备来源. * 客户端设备识别:识别结果只有3种类型:NORMAL(非手机设备)、MOBILE(手机设备)、TABLET(平板电脑)。在系统里可以通过以下代码获取设备识别结果: * 网站偏好设置:Spring 通过设备识别的结果来设置当前网站是NORMAL还是MOBILE。 * 最后 Spring Mobile会将信息同时放入cookie和request attribute里面。 * 网站自动切换:可根据不同的访问设备切换到对应的页面 * 此处需要在web.xml中配置DeviceResolverRequestFilter过滤器 * @param request 请求对象 */ private void obtainDeviceOrigin(HttpServletRequest request) { Device currentDevice = DeviceUtils.getCurrentDevice(request); if (currentDevice.isMobile()) { this.deviceOrigin = DeviceOrigin.MOBILE; } if (currentDevice.isNormal()) { this.deviceOrigin = DeviceOrigin.NORMAL; } if (currentDevice.isTablet()) { this.deviceOrigin = DeviceOrigin.TABLET; } } /** * 比较对象的IP地址. * * @param rhs 被比较的对象 * @return 布尔值 */ public boolean isEqualsRemoteAddress(WAPAuthenticationDetails rhs) { if ((remoteAddress == null) && (rhs.getRemoteAddress() != null)) { return false; } if ((remoteAddress != null) && (rhs.getRemoteAddress() == null)) { return false; } if (remoteAddress != null && !remoteAddress.equals(rhs.getRemoteAddress())) { return false; } return true; } /** * 比较对象的会话ID. * * @param rhs 被比较的对象 * @return 布尔值 */ public boolean isEqualsSessionId(WAPAuthenticationDetails rhs) { if ((sessionId == null) && (rhs.getSessionId() != null)) { return false; } if ((sessionId != null) && (rhs.getSessionId() == null)) { return false; } if (sessionId != null && !sessionId.equals(rhs.getSessionId())) { return false; } return true; } @Override public boolean equals(Object obj) { if (obj instanceof WAPAuthenticationDetails) { WAPAuthenticationDetails rhs = (WAPAuthenticationDetails) obj; if (isEqualsRemoteAddress(rhs) && isEqualsSessionId(rhs)) { return true; } } return false; } @Override public int hashCode() { int code = HASH_CODE; if (this.remoteAddress != null) { code = code * (this.remoteAddress.hashCode() % SEVEN); } if (this.sessionId != null) { code = code * (this.sessionId.hashCode() % SEVEN); } return code; } }
WAPAuthenticationDetailsSource
/** * Created by leitao on 2016/12/22. * WAP 认证细节源对象 */ public class WAPAuthenticationDetailsSource implements AuthenticationDetailsSource, WAPAuthenticationDetails> { @Override public WAPAuthenticationDetails buildDetails(HttpServletRequest context) { return new WAPAuthenticationDetails(context); } }
登录成功后获取用户资料:
/** * Created by leitao on 2016/12/28. * Wap上下文工具 */ public class WapContextUtils { /** * 获取WAP登录用户(也就是当前会话中的用户). * @return 用户信息 */ public static MobileUser getMobileUser() { MobileUser user = null; if (SecurityContextHolder.getContext().getAuthentication().getPrincipal() instanceof MobileUser) { user = (MobileUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); } return user; } }特别提醒用户bean一定要集成Spring Security提供的User类,这样Spring Security才会帮我们管理该对象;
切入点:
/** * Created by leitao on 2016/12/21. * WAP登录表单地址认证切入点 */ public class WAPLoginUrlAuthenticationEntryPoint extends LoginUrlAuthenticationEntryPoint { /** * 创建新的实例 * @param loginFormUrl 登录表单地址 */ public WAPLoginUrlAuthenticationEntryPoint(String loginFormUrl){ super(loginFormUrl); } }单点登陆成功后:
/** * Created by leitao on 2016/12/21. * SSO WAP认证成功处理器 */ public class WAPAuthenticationSuccessHandler implements AuthenticationSuccessHandler { /** * 日志对象 */ Logger log = LogManager.getLogger(getClass()); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { log.info("进入WAP认证成功处理器"); RedirectUrlBuilder builder = new RedirectUrlBuilder(); builder.setScheme("http"); builder.setServerName(request.getServerName()); builder.setPort(request.getServerPort()); builder.setContextPath(request.getContextPath()); String returnUrl = builder.getUrl(); Object object = request.getSession().getAttribute("returnUrl"); if (!StringUtils.isEmpty(object)){ returnUrl = object.toString(); request.getSession().removeAttribute("returnUrl"); } if (!StringUtils.isEmpty(returnUrl)){ log.info("认证成功后跳转地址returnUrl:"+returnUrl); response.sendRedirect(returnUrl); return; }else { request.getRequestDispatcher("/").forward(request,response); } } }跳转到相应的地址;
此时Spring Security会把SPRING_SECURITY_CONTEXT 存入Session中,也可以通过上面我提供的方法的从上下文中获取用户资料