生活加油:摘一句子:
“我希望自己能寫這樣的詩。我希望自己也是一顆星星。如果我會發光,就不必害怕黑暗。如果我自己是那么美好,那么一切恐懼就可以煙消雲散。於是我開始存下了一點希望—如果我能做到,那么我就戰勝了寂寞的命運。”
-----------------------------王小波《我在荒島上迎接黎明》
————————————————
嗯,剛學的時候,一大堆,明白是配置了啥,不知道為啥那么配,嗯,今天把配置思想和小伙伴分享,是很簡單的那種,不足之處請留言.
SpringSecurity的配置基於WebSecurityConfigurerAdapter的實現類,我們這里主要講基本配置,即configure(HttpSecurity http)方法的配置,其實大都有默認值,我們可以直接用默認值,也可以自己設置.
私以為簡單配置三點:
- 認證(SpringSecurity在第一次登錄做認證,這時候只進行認證,不進行授權(鑒權處理))
- 表單認證:涉及到的配置包括前端登錄請求的url,登錄失敗,登錄成功返回的頁面url,已及表單返回字段的用戶名密碼K的設置,還有登陸成功回調處理和登錄失敗回調處理.這里要注意登錄數據是通過key/value的形式來傳遞.
- 自定義認證,(驗證碼):自定義驗證采用,在自定義過濾器的方式,在最開始插入一個過濾器鏈,需要自定義異常,自定義失敗登錄失敗處理器,注意這里的失敗處理器要和表單登錄使用同一個失敗處理器.
- 授權(基於RBAC權限控制):
- 授權這樣分析基於RBAC的權限控制,即用戶分配角色,角色分配權限.需要配置根據訪問路徑得到需要的角色的處理類(FilterlnvocationSecurityrMetadataSource),根據返回角色進行鑒權處理類(AccessDecisionManager) , 同時判斷是否登錄,還需要配置認證失敗的處理器.
- 注銷登錄
- 注銷登錄默認的url為"/logout".需要配置,注銷登錄處理器
配置圖:
SpringSecurity簡單配置圖:
下面就上面講的代碼分析
一.認證:
1.configure(HttpSecurity http)方法的配置:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.liruilong.hros.filter.VerifyCodeFilter; import com.liruilong.hros.model.Hr; import com.liruilong.hros.model.RespBean; import com.liruilong.hros.service.HrService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.*; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @Description : * @Author: Liruilong * @Date: 2019/12/18 19:11 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired HrService hrService; @Autowired CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource; @Autowired CustomUrlDecisionManager customUrlDecisionManager; @Autowired VerifyCodeFilter verifyCodeFilter ; @Autowired MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class) .authorizeRequests() //.anyRequest().authenticated() //所有請求的都會經過這進行鑒權處理。返回當前請求需要的角色。 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O object) { object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource); object.setAccessDecisionManager(customUrlDecisionManager); return object; } }) .and().formLogin().usernameParameter("username").passwordParameter("password") //設置登錄請求的url路徑 .loginProcessingUrl("/doLogin") /*需要身份驗證時,將瀏覽器重定向到/ login 我們負責在請求/ login時呈現登錄頁面 當身份驗證嘗試失敗時,將瀏覽器重定向到/ login?error(因為我們沒有另外指定) 當請求/ login?error時,我們負責呈現失敗頁面 成功注銷后,將瀏覽器重定向到/ login?logout(因為我們沒有另外指定) 我們負責在請求/ login?logout時呈現注銷確認頁面*/ .loginPage("/login") //登錄成功回調 .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); Hr hr = (Hr) authentication.getPrincipal(); //密碼不回傳 hr.setPassword(null); RespBean ok = RespBean.ok("登錄成功!", hr); //將hr轉化為Sting String s = new ObjectMapper().writeValueAsString(ok); out.write(s); out.flush(); out.close(); } }) //登失敗回調 .failureHandler(myAuthenticationFailureHandler) //相關的接口直接返回 .permitAll().and().logout() //注銷登錄 .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注銷成功!"))); out.flush(); out.close(); } }) .permitAll().and().csrf().disable().exceptionHandling() //沒有認證時,在這里處理結果,不要重定向 .authenticationEntryPoint( //myAuthenticationEntryPoint; new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); resp.setStatus(401); PrintWriter out = resp.getWriter(); RespBean respBean = RespBean.error("訪問失敗!"); if (authException instanceof InsufficientAuthenticationException) { respBean.setMsg("請求失敗,請聯系管理員!"); } out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } }); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(hrService); } /** * @Author Liruilong * @Description 放行的請求路徑 * @Date 19:25 2020/2/7 * @Param [web] * @return void **/ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/auth/code","/login","/css/**","/js/**", "/index.html", "/img/**", "/fonts/**","/favicon.ico"); } }
2.失敗處理器定義:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.liruilong.hros.Exception.ValidateCodeException; import com.liruilong.hros.model.RespBean; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.*; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @Description : * @Author: Liruilong * @Date: 2020/2/11 23:08 */ @Component public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); RespBean respBean = RespBean.error(e.getMessage()); // 驗證碼自定義異常的處理 if (e instanceof ValidateCodeException){ respBean.setMsg(e.getMessage()); //Security內置的異常處理 }else if (e instanceof LockedException) { respBean.setMsg("賬戶被鎖定請聯系管理員!"); } else if (e instanceof CredentialsExpiredException) { respBean.setMsg("密碼過期請聯系管理員!"); } else if (e instanceof AccountExpiredException) { respBean.setMsg("賬戶過期請聯系管理員!"); } else if (e instanceof DisabledException) { respBean.setMsg("賬戶被禁用請聯系管理員!"); } else if (e instanceof BadCredentialsException) { respBean.setMsg("用戶名密碼輸入錯誤,請重新輸入!"); } //將hr轉化為Sting out.write(new ObjectMapper().writeValueAsString(respBean)); out.flush(); out.close(); } @Bean public MyAuthenticationFailureHandler getMyAuthenticationFailureHandler(){ return new MyAuthenticationFailureHandler(); } }
3.前端將JSON數據轉換為K-V形式:
//post請求的封裝K-v形式 let base = ''; export const postKeyValueRequest = (url, params) => { return axios({ method: 'post', url: `${base}${url}`, data: params, transformRequest: [function (data) { let ret = '' for (let it in data) { ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' } return ret }], headers: { 'Content-Type': 'application/x-www-form-urlencoded', } }); }
4.自定義過濾器:
package com.liruilong.hros.filter; import com.liruilong.hros.Exception.ValidateCodeException; import com.liruilong.hros.config.MyAuthenticationFailureHandler; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; /** * @Description : * @Author: Liruilong * @Date: 2020/2/7 19:39 */ @Component public class VerifyCodeFilter extends OncePerRequestFilter { @Bean public VerifyCodeFilter getVerifyCodeFilter() { return new VerifyCodeFilter(); } @Autowired MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equals("/doLogin", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) { // 1. 進行驗證碼的校驗 try { String requestCaptcha = request.getParameter("code"); if (requestCaptcha == null) { throw new ValidateCodeException("驗證碼不存在"); } String code = (String) request.getSession().getAttribute("yanzhengma"); if (StringUtils.isBlank(code)) { throw new ValidateCodeException("驗證碼過期!"); } code = code.equals("0.0") ? "0" : code; logger.info("開始校驗驗證碼,生成的驗證碼為:" + code + " ,輸入的驗證碼為:" + requestCaptcha); if (!StringUtils.equals(code, requestCaptcha)) { throw new ValidateCodeException("驗證碼不匹配"); } } catch (AuthenticationException e) { // 2. 捕獲步驟1中校驗出現異常,交給失敗處理類進行進行處理 myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e); } finally { filterChain.doFilter(request, response); } } else { filterChain.doFilter(request, response); } } }
5.自定義異常:
package com.liruilong.hros.Exception; import org.springframework.security.core.AuthenticationException; /** * @Description : * @Author: Liruilong * @Date: 2020/2/8 7:24 */ public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) { super(msg); } }
二.授權:
RBAC模型實體圖
1.開發者自定義 FilterlnvocationSecurityrMetadataSource
package com.liruilong.hros.config; import com.liruilong.hros.mapper.MenuMapper; import com.liruilong.hros.model.Menu; import com.liruilong.hros.model.Role; import com.liruilong.hros.service.MenuService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.stereotype.Component; import org.springframework.util.AntPathMatcher; import java.util.Collection; import java.util.List; /** * @Description : 權限處理,根據請求,分析需要的角色,該類的主要功能就是通過當前的請求地址,獲取該地址需要的用戶角色 * @Author: Liruilong * @Date: 2019/12/24 12:17 */ @Component public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired MenuService menuService; //路徑比較工具 AntPathMatcher antPathMatcher = new AntPathMatcher(); /** * @return java.util.Collection<org.springframework.security.access.ConfigAttribute> * @Author Liruilong * @Description 當前請求需要的角色 * @Date 18:13 2019/12/24 * @Param [object] **/ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { //獲取當前請求路徑 String requestUrl = ((FilterInvocation) object).getRequestUrl(); //獲取所有的菜單url路徑 List<Menu> menus = menuService.getAllMenusWithRole(); for (Menu menu : menus) { if (antPathMatcher.match(menu.getUrl(), requestUrl)) { //擁有當前菜單權限的角色 List<Role> roles = menu.getRoles(); String[] strings = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { strings[i] = roles.get(i).getName(); } return SecurityConfig.createList(strings); } } // 沒匹配上的資源都是登錄 return SecurityConfig.createList("ROLE_LOGIN"); } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
2.自定義 AccessDecisionManager
package com.liruilong.hros.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; import java.util.Collection; /** * @Description : 判斷當前用戶是否具備菜單訪問,當一個請求走完 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法后,接下來就會 來到 AccessDecisionManager 類中進行角色信息的比對 * @Author: Liruilong * @Date: 2019/12/24 19:12 */ @Component public class CustomUrlDecisionManager implements AccessDecisionManager { /** * @return void * @Author Liruilong * @Description decide 方法有三個參數, 第一個參數包含當前登錄用戶的信息; * 第二個參數則是一個 Filterlnvocation 對 象 ,可以 獲 取當前請求對 象等; * 第 三個參 數就是 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法的返回值, 即當前請求 URL 所 需要的角色。 * @Date 18:28 2020/2/13 * @Param [authentication, object, configAttributes] **/ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { for (ConfigAttribute configAttribute : configAttributes) { String needRole = configAttribute.getAttribute(); if ("ROLE_LOGIN".equals(needRole)) { //判斷用戶是否登錄 if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException("尚未登錄,請登錄!"); } else { return; } } Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(needRole)) { return; } } } throw new AccessDeniedException("權限不足,請聯系管理員!"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } } // authorities.stream().anyMatch((authority) ->authority.getAuthority().equals(attribute));
RBAC模型分析
3. 當然還需要用戶類去實現UserDatails接口
package com.liruilong.hros.model; import com.fasterxml.jackson.annotation.JacksonInject; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreType; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class Hr implements UserDetails { private Integer id; private String name; private String phone; private String telephone; private String address; private Boolean enabled; private String username; private String password; private String userface; private String remark; private List<Role> roles; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name == null ? null : name.trim(); } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone == null ? null : phone.trim(); } public String getTelephone() { return telephone; } public void setTelephone(String telephone) { this.telephone = telephone == null ? null : telephone.trim(); } public String getAddress() { return address; } public void setAddress(String address) { this.address = address == null ? null : address.trim(); } public void setEnabled(Boolean enabled) { this.enabled = enabled; } @Override public String getUsername() { return username; } public void setUsername(String username) { this.username = username == null ? null : username.trim(); } @Override public String getPassword() { return password; } public void setPassword(String password) { this.password = password == null ? null : password.trim(); } public String getUserface() { return userface; } public void setUserface(String userface) { this.userface = userface == null ? null : userface.trim(); } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark == null ? null : remark.trim(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return enabled; } @Override @JsonIgnore public Collection<? extends GrantedAuthority> getAuthorities() { List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size()); roles.stream().forEach( (role) ->authorities.add(new SimpleGrantedAuthority(role.getName()))); return authorities; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } }
_______________________________________________________________________________________________________
嗯.以上就是對SpringSecurity配置的理解,不足之處請小伙伴留言/生活加油!