Spring Security 实现如下功能(摘要格式显示太丑了放在下面)
1》JDBC 数据库认证 (UserDetailsService)
2》系统权限控制 (GrantedAuthority)
3》自定义登陆界面以及登陆提示 (MessageSource)
4》登陆验证码认证 (UsernamePasswordAuthenticationFilter)
5》Session 使用 Redis 作存储 (SecurityContext 实现) css
很久没有写博文了,放假闲在家里整了整 Spring Security 的集群Session没有同步的问题,整了好几天没整好最后使用了一种比较 Low 的实现方式,这篇文章是给一些不懂Spring Security得朋友看的当作入门级的教程吧。html
教程的运行流程图,回头补上。留下时间为证 2018-4-7 13:53:30java
上码撸起 走你~ web
第一部分->用户实现redis
自定义用户须要实现UserDetails 的接口,使用了@Data 还用Get Set 只是为了UserName 不跟UserDetails 中的登陆名称冲突spring
@Data class UserInfo implements UserDetails { public Log logger = LogFactory.getLog(UserInfo.class); private String userName; private String password; List<MyGrantedAuthority> list; //用户所拥有的权限 GrantedAuthority public UserInfo() { } public UserInfo(String userName, String password) { this.userName = userName; this.password = password; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.list; } @Override public String getPassword() { return this.userName; } @Override public String getUsername() { return this.password; } @Override public boolean isAccountNonExpired() {//指示用户的账户是否已过时。 return true; } @Override public boolean isAccountNonLocked() {//指示用户是锁定仍是解锁。 return true; } @Override public boolean isCredentialsNonExpired() { //指示用户的凭证(密码)是否已过时。 return true; } @Override public boolean isEnabled() { //指示用户是启用仍是禁用。 return true; } public Log getLogger() { return logger; } public void setLogger(Log logger) { this.logger = logger; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public void setPassword(String password) { this.password = password; } public List<MyGrantedAuthority> getList() { return list; } public void setList(List<MyGrantedAuthority> list) { this.list = list; } }
自定义登陆数据库
这个把资源赋值给用户实际是要在登陆成功以后执行的代码,Demo 就将就的看看吧,比较是学习用的,只要掌握流程便好。apache
public class MyUserDetailsService implements UserDetailsService { public Log logger = LogFactory.getLog(MyUserDetailsService.class); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("\n\n ------> > Login Find By UserName :" + username +"\n\n"); UserInfo user = new UserInfo("admin", "admin123"); if(!"admin".equals(username)){ throw new UsernameNotFoundException("用户不存在"); } user.setList(new ArrayList<MyGrantedAuthority>(Arrays.asList(new MyGrantedAuthority[]{ new MyGrantedAuthority(1,"F1"), new MyGrantedAuthority(1,"F2"), new MyGrantedAuthority(1,"F3"), }))); return user; } }
第二部分->用户权限api
自定义权限须要实现GrantedAuthority的接口cookie
@Data class MyGrantedAuthority implements GrantedAuthority{ private Integer id; private String code; public MyGrantedAuthority() { } public MyGrantedAuthority(Integer id, String code) { this.id = id; this.code = code; } @Override public String getAuthority() { return code; } }
第三部分->验证码确认
验证码认证明现
/** * 验证码 * 验证码认证 -> 可改用 Redis 实现。 */ protected class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public LoginAuthenticationFilter() { super(); setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(login_failure)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if ("POST".equalsIgnoreCase(req.getMethod()) && login.equals(req.getServletPath())) { String vcode = req.getParameter("vcode"); if (vcode != null && !vcode.equalsIgnoreCase("X1234")) { unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("VCode Error")); return; } } chain.doFilter(request, response); } }
验证码实现加入到SecurityContext 上下文中
http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
第四部分->核心配置理解
Session 管理使用了最Low 的解决方案 ,在登陆成功以后把 SecurityContext 存储到Redis中,而后在全部请求致以前进行拦截经过Sid 判断是否登陆,没有登陆而且有Sid 则去Redis 中寻找SecurityContext 而后加入到系统当中。
SecurityContext 不能用FastJson 作序列化,序列化以后反序列化会出现数据不对的状况。这里所使用的是Io流加Base64的方式序列化成字符串而后存入Redis 而后反序列化出来的。
Spring Mvc 加入 Spring Security 的两种方式
1》Java Config 继承 AbstractSecurityWebApplicationInitializer
2》Web Xml Config
说明:两种方式都是为了注册一个 springSecurityFilterChain 的拦截器,只能使用一种配置,不然会报错,出现两个springSecurityFilterChain 拦截器的。
<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>
Spring Security 核心配置 过滤器
package com.pw.test.controller.config; import cn.hutool.core.codec.Base64; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; import java.util.Locale; import java.util.UUID; import java.util.concurrent.TimeUnit; /*** * <pre> * * Web.xml 配置 * * <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> * * Spring Security 实现功能 * SecurityContext 实现 Redis 存储 , 解决Security 集群Session 不统一的问题 * Redis 存储解决方案 * SavedRequestAwareAuthenticationSuccessHandler -> SecurityContext 先序列化 转 Base64 字符串 存储到 Redis 中 * HttpSessionSecurityContextRepository -> 拦截全部的请求 , 若是有SID 则用SID 到Redis 中取数据 , 而后反序列化 存入 HTTPSession 中 * SecurityContextLogoutHandler -> 退出操做 删除 Redis 中的数据 * * 实现验证码功能。 * 实现登陆提示中文化。 * * * * </pre> */ @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { public Log logger = LogFactory.getLog(MyUserDetailsService.class); public String login = "/login"; public String login_out = "/logout"; public String login_out_url = "/"; public String login_success = "/success"; public String login_failure = "/login?error=true"; @Autowired private StringRedisTemplate redisTemplate; /** * 全部请求以前 * 反写到HttpSession 中 */ public class MyHttpSessionSecurityContextRepository extends HttpSessionSecurityContextRepository { public MyHttpSessionSecurityContextRepository() { super(); } @Override public SecurityContext loadContext(HttpRequestResponseHolder holder) { SecurityContext context = super.loadContext(holder); //TODO 反写 SecurityContext if(null == context || null == context.getAuthentication() || null == context.getAuthentication().getPrincipal()) { //已登陆用户 String sid = getSid(holder.getRequest().getCookies()); if(null != sid){ String contextString = redisTemplate.opsForValue().get(sid); if(null != contextString){ context = (SecurityContext)fromSerializableString(contextString); } } } return context; } } /** * 退出操做 * 删除 Redis 中的数据 */ public class MySecurityContextLogoutHandler extends SecurityContextLogoutHandler { //TODO 登出 删除 Redis 的Sid @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String sid = getSid(request.getCookies()); redisTemplate.opsForValue().getOperations().delete(sid); super.logout(request, response, authentication); } } /** * 登陆成功 * 登录成功把SecurityContext 存储到 Redis 中 */ protected class SuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler { //TODO SecurityContext 写入到 Redis 中 @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { SecurityContext context = SecurityContextHolder.getContext(); if(null != context && null != context.getAuthentication() && null != context.getAuthentication().getPrincipal()) { //已登陆用户 String sid = getSid(request.getCookies()); if(null == sid){ sid = UUID.randomUUID().toString(); response.addCookie(new Cookie("sid",sid)); } redisTemplate.opsForValue().set(sid, toSerializableString(context), 180, TimeUnit.SECONDS); } super.onAuthenticationSuccess(request, response, authentication); } } /** * 验证码 * 验证码认证 -> 可改用 Redis 实现。 */ protected class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public LoginAuthenticationFilter() { super(); setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(login_failure)); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; if ("POST".equalsIgnoreCase(req.getMethod()) && login.equals(req.getServletPath())) { String vcode = req.getParameter("vcode"); if (vcode != null && !vcode.equalsIgnoreCase("X1234")) { unsuccessfulAuthentication(req, res, new InsufficientAuthenticationException("VCode Error")); return; } } chain.doFilter(request, response); } } /*** * 登陆错误提示 中文 * org.springframework.security.messages * @return */ @Bean public MessageSource messageSource() { Locale.setDefault(Locale.SIMPLIFIED_CHINESE); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.addBasenames("classpath:org/springframework/security/messages"); return messageSource; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(new MyUserDetailsService()).passwordEncoder(new PasswordEncoder() { public String encode(CharSequence rawPassword) { return null; } public boolean matches(CharSequence rawPassword, String encodedPassword) { logger.info("\n\n------> > matches CharSequence :" + rawPassword.toString() + "\t\t encodedPassword:" + encodedPassword + "\n\n"); return rawPassword.toString().equals(encodedPassword); } }); } @Override protected void configure(HttpSecurity http) throws Exception { logger.info("\n\n ---> ---> WebSecurityConfig extends WebSecurityConfigurerAdapter configure(HttpSecurity http) \n\n"); http.securityContext().securityContextRepository(new MyHttpSessionSecurityContextRepository()); http.sessionManagement().enableSessionUrlRewriting(true); http.csrf().disable(); http .authorizeRequests() //方法有多个子节点,每一个macher按照他们的声明顺序执行。 .antMatchers("/css/**", "/signup", "/about").permitAll() //咱们指定任何用户均可以访问多个URL的模式。 任何用户均可以访问以"/resources/","/signup", 或者 "/about"开头的URL。 .antMatchers("/admin/**").hasRole("ADMIN") //以 "/admin/" 开头的URL只能由拥有 "ROLE_ADMIN"角色的用户访问。请注意咱们使用 hasRole 方法,没有使用 "ROLE_" 前缀 .anyRequest().authenticated() //是对http全部的请求必须经过受权认证才能够访问。 .and() .formLogin() //指定登陆页的路径 .loginPage(login) //自定义登陆页页面 .usernameParameter("userLoginName") //登陆名参数必须被命名为userLoginName .passwordParameter("userLoginPassword") //密码参数必须被命名为userLoginPassword .defaultSuccessUrl(login_success) .successHandler(new SuccessHandler()) // // .defaultSuccessUrl("/index") //登陆成功后处理页面 // .successHandler(new AuthenticationSuccessHandler() { //登陆成功后处理 // public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // // } // }) .failureUrl(login_failure) //登陆失败后处理页面 // .failureHandler(new MyAuthenticationFailureHandler()) // .failureHandler(new AuthenticationFailureHandler() { //登陆失败后处理 // @Override // public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // // } // }) .permitAll() //咱们必须容许全部用户访问咱们的登陆页(例如为验证的用户),这个formLogin().permitAll()方法容许基于表单登陆的全部的URL的全部用户的访问。 .and() .logout() //提供注销支持,使用WebSecurityConfigurerAdapter会自动被应用 .logoutUrl(login_out) //设置触发注销操做的URL (默认是/logout). 若是CSRF内启用(默认是启用的)的话这个请求的方式被限定为POST。 请查阅相关信息 JavaDoc相关信息. .logoutSuccessUrl(login_out_url) //注销以后跳转的URL。默认是/login?logout。具体查看 the JavaDoc文档. // .logoutSuccessHandler(logoutSuccessHandler) //让你设置定制的 LogoutSuccessHandler。若是指定了这个选项那么logoutSuccessUrl()的设置会被忽略。请查阅 JavaDoc文档. // .invalidateHttpSession(true) //指定是否在注销时让HttpSession无效。 默认设置为 true。 在内部配置SecurityContextLogoutHandler选项。 请查阅 JavaDoc文档. .addLogoutHandler(new MySecurityContextLogoutHandler()) //添加一个LogoutHandler.默认SecurityContextLogoutHandler会被添加为最后一个LogoutHandler。 // .deleteCookies(cookieNamesToClear) //容许指定在注销成功时将移除的cookie。这是一个现实的添加一个CookieClearingLogoutHandler的快捷方式。 .permitAll() ; http.addFilterBefore(new LoginAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } public static String getSid(Cookie[] cookies){ if(null != cookies &&cookies.length > 0){ for (Cookie cookie : cookies) { if("sid".equals(cookie.getName())){ return cookie.getValue(); } } } return null; } /** * Read the object from Base64 string. */ private static Object fromSerializableString(String s) { try { byte[] data = Base64.decode(s); ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); Object o = ois.readObject(); ois.close(); return o; }catch (Exception e){ e.printStackTrace(); } return null; } /** * Write the object to a Base64 string. */ private static String toSerializableString(Object o) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(o); oos.close(); return Base64.encode(baos.toByteArray()); }catch (Exception e){ e.printStackTrace(); } return null; } }