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; } }