spring security入門demo
一、前言
因專案需要引入spring security許可權框架,而之前也沒接觸過這個一門,於是就花了點時間弄了個小demo出來,說實話,剛開始接觸這個確實有點懵,看網上資料寫的許可權大都是靜態,即就是在配置檔案或程式碼裡面寫定角色,不能動態更改,個人感覺這樣實際場景應該應用的不多,於是就進一步研究,整理出了一個可以動態管理個人許可權角色demo,其中可能有很多不足或之處,還望指正。本文通過spring boot整合spring security,處理方式沒有使用xml檔案格式,而是用了註解。
二、表結構
接觸過許可權這塊的,大都應該知道,最核心的有三張表(當然,如果牽涉業務複雜,可能不止)。
一、使用者表
二、角色表
三、選單表(即許可權表)
剩餘還有兩張多對多的表。即使用者與角色,角色與選單。如下圖
三、spring security入口
由於本文只是著重說spring security,關於spring boot一塊內容會直接帶過。如spring boot啟動類配置等。
首先會自定義一個類去實現WebSecurityConfigurerAdapter類。重寫其中幾個方法,程式碼如下
1 @Configuration 2 public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 3 4@Autowired 5@Qualifier(value = "userDetailServiceImpl") 6private UserDetailsService userDetailsService; 7 8@Autowired 9private LoginSuccessAuthenticationHandler successAuthenticationHandler; 10 11@Autowired 12private LoginFailureAuthenticationHandler failureAuthenticationHandler; 13 14@Autowired 15private AuthenticationAccessDeniedHandler accessDeniedHandler; 16 17@Autowired 18private UrlAccessDecisionManager decisionManager; 19 20@Autowired 21private UrlPathFilterInvocationSecurityMetadataSource urlPathFilterInvocationSecurityMetadataSource; 22 23@Autowired 24private AuthenticationProvider authenticationProvider; 25 26@Autowired 27private PasswordEncoder passwordEncoder; 28 29@Override 30protected void configure(AuthenticationManagerBuilder auth) throws Exception { 31auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder); 32auth.authenticationProvider(authenticationProvider); 33} 34 35@Override 36public void configure(WebSecurity web) { 37web.ignoring().antMatchers("/index.html","/favicon.ico"); 38} 39 40@Override 41protected void configure(HttpSecurity http) throws Exception { 42http.csrf().disable() 43.authorizeRequests() 44.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { 45@Override 46public <O extends FilterSecurityInterceptor> O postProcess(O o) { 47o.setAccessDecisionManager(decisionManager); 48o.setSecurityMetadataSource(urlPathFilterInvocationSecurityMetadataSource); 49return o; 50} 51}) 52 53.anyRequest() 54.authenticated()// 其他 url 需要身份認證 55 56.and() 57.formLogin()//開啟登入,如果不指定登入路徑(即輸入使用者名稱和密碼錶單提交的路徑),則會預設為spring securtiy的內部定義的路徑 58.successHandler(successAuthenticationHandler) 59.failureHandler(failureAuthenticationHandler)// 遇到使用者名稱或密碼不正確/使用者被鎖定等情況異常,會交給此handler處理 60.permitAll() 61 62.and() 63.logout() 64.logoutUrl("/logout")//退出操作,其實也有一個handler,如果沒其他業務邏輯,可以預設為spring security的handler 65.permitAll() 66.and() 67.exceptionHandling().accessDeniedHandler(accessDeniedHandler); 68}
在這裡會介紹以下幾個類作用
一、UserDetailsService 二、AuthenticationProvider 三、AuthenticationAccessDeniedHandler 四、UrlAccessDecisionManager 五、UrlPathFilterInvocationSecurityMetadataSource 至於LoginSuccessAuthenticationHandler、LoginFailureAuthenticationHandler就是用來處理登入成功和登入失敗情況,這裡不做介紹
3.1、UserDetailService的作用
這個一個介面,通常我們需要去實現它,作用主要是用來我們和資料庫做互動用的。簡單來說,就是使用者名稱傳過來,這個類負責校驗使用者名稱是否存在等業務邏輯。
1 @Component 2 public class UserDetailServiceImpl implements UserDetailsService { 3 4@Autowired 5private SysUserDAO userDAO; 6 7@Autowired 8private PasswordEncoder passwordEncoder; 9 10 11@Override 12public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 13SysUser sysUser = userDAO.findByUsername(s); 14if (sysUser == null){ 15throw new UsernameNotFoundException("使用者不存在"); 16} 17String pwd = passwordEncoder.encode(sysUser.getPassword()); 18System.out.println(pwd); 19return new User(sysUser.getUsername(),pwd,getRoles(sysUser.getRoles())); 20} 21 22private Collection<GrantedAuthority> getRoles(List<SysRole> roles){ 23List<GrantedAuthority> list = new ArrayList<>(); 24for (SysRole role : roles){ 25SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); 26list.add(grantedAuthority); 27} 28return list; 29} 30 }
程式碼比較簡單,值得注意的是sercurity裡的User物件,它的一個建構函式有是哪個引數值,第一個和第二個是使用者名稱和密碼,密碼作用就是後面用來校驗前端傳過來的密碼正確性。稍後會講到。至於第三個引數就是當前使用者所擁有的角色,作用就是在當前端請求一個介面的時候,會判斷這個介面所擁有的許可權和該使用者所有的許可權有重合,簡單來說就是該使用者是否擁有該介面許可權。這裡也就實現了一個角色可以動態修改的功能。因其實從資料庫查詢出來。
3.2、AuthenticationProvider
它也是一個介面,它的作用是用來校驗使用者密碼等功能,當然如簡訊驗證或要第三方驗證,也可以實現這個介面,在本文中是用密碼校驗。前面也說到userDetailService會傳一個使用者的基本資訊。它的主要作用就是為該介面服務的。
1 @Component 2 public class LoginAuthenticationProvider implements AuthenticationProvider { 3 4@Autowired 5private UserDetailsService userDetailsService; 6 7@Override 8public Authentication authenticate(Authentication authentication) throws AuthenticationException { 9// 獲取表單使用者名稱 10String username = (String) authentication.getPrincipal(); 11// 獲取表單使用者填寫的密碼 12String password = (String) authentication.getCredentials(); 13 14UserDetails userDetails = userDetailsService.loadUserByUsername(username); 15 16String password1 = userDetails.getPassword(); 17if (!Objects.equals(password,password1)){ 18throw new BadCredentialsException("使用者名稱或密碼不正確"); 19} 20 21return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities()); 22} 23 24@Override 25public boolean supports(Class<?> aClass) { 26return true; 27} 28 }
值得注意的是如果驗證通過會返回一個UsernamePasswordAuthenticationToken物件,它的作用就是標誌著此使用者已通過登入驗證,如果沒通過,則spring security會捕捉如程式碼18行的異常,然後再包裝一個匿名的token,即AnonymousAuthenticationToken,此token即代表使用者未登入。兩個介面主要服務於使用者登入這塊。接下來的三個是服務於許可權校驗。即介面驗證
3.3、UrlPathFilterInvocationSecurityMetadataSource
它的作用是用來處理當前使用者是否擁有此介面的許可權。
1 @Component 2 public class UrlPathFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { 3 4 5@Autowired 6private SysMenuDAO sysMenuDAO; 7 8private AntPathMatcher antPathMatcher = new AntPathMatcher(); 9 10@Override 11public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { 12FilterInvocation filterInvocation = (FilterInvocation) object; 13String requestUrl = filterInvocation.getRequestUrl(); 14// 因為選單一般隨著開發完成,變動不大,此處可以使用快取,這裡為了演示,就直接查庫,選單對應角色需要動態情快取,如變更選單和角色關係,需清除快取 15List<SysMenu> all = sysMenuDAO.findAll(); 16for (SysMenu menu : all) { 17if (menu.getRoles().size() != 0 && antPathMatcher.match(menu.getUrlPath(), requestUrl)) { 18List<SysRole> roles = menu.getRoles(); 19int size = roles.size(); 20String[] values = new String[size]; 21for (int i = 0; i < size; i++) { 22values[i] = roles.get(i).getRoleName(); 23} 24return SecurityConfig.createList(values); 25} 26} 27return SecurityConfig.createList("ROLE_LOGIN"); 28} 29 30@Override 31public Collection<ConfigAttribute> getAllConfigAttributes() { 32return null; 33} 34 35@Override 36public boolean supports(Class<?> clazz) { 37return true; 38} 39 }
從程式碼就可以看出16行的for迴圈就是獲取當前請求介面鎖需要的許可權,這裡使用spring security的路徑匹配類。如果該介面·沒有許可權,這裡返回一個標誌如ROLE_LOGIN,當然如果需要其他標誌可以自行定義,這裡為了簡便,就用了這個。
3.4、UrlAccessDecisionManager
這個類就是最終的決策類。從3.1到3.2,大家都清楚,已有的資訊,使用者所有的許可權這個已經獲取到了,3.3可知當前請求介面的許可權也已經獲取到了,剩下的肯定就是比較兩這個許可權集合有沒有交集,如果有則表明當前使用者擁有此介面的許可權。
1 @Component 2 public class UrlAccessDecisionManager implements AccessDecisionManager { 3 4/** 5* 6* @param authentication 當前使用者資訊,和當前使用者的擁有許可權資訊,即來自於userDetailService裡的 7* @param object 即FilterInvocation物件,可以獲取httpServletRequest請求物件 8* @param configAttributes本次訪問所需要的許可權 9* @throws AccessDeniedException 10* @throws InsufficientAuthenticationException 11*/ 12@Override 13public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { 14Iterator<ConfigAttribute> iterator = configAttributes.iterator(); 15while (iterator.hasNext()) { 16ConfigAttribute ca = iterator.next(); 17//當前請求需要的許可權 18String needRole = ca.getAttribute(); 19if ("ROLE_LOGIN".equals(needRole)) { 20// 即匿名使用者/未登入,如果使用者登入成功。那麼authententication就是前面提到的UsernamePasswordAuthententicationToken類 21if (authentication instanceof AnonymousAuthenticationToken) { 22throw new BadCredentialsException("未登入"); 23} else {// 登入但不具有此路徑許可權,即前面3.3提到的ROLE_LOGIN,介面沒有角色對應,主要使用者已經登入成功 24break; 25} 26} 27//當前使用者所具有的許可權 28Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); 29for (GrantedAuthority authority : authorities) { 30if (authority.getAuthority().equals(needRole)) { 31return; 32} 33} 34} 35throw new AccessDeniedException("許可權不足!"); 36} 37 38@Override 39public boolean supports(ConfigAttribute attribute) { 40return true; 41} 42 43@Override 44public boolean supports(Class<?> clazz) { 45return true; 46} 47 }
3.5、 AuthenticationAccessDeniedHandler
這個類就是用來接收上面丟擲的accessDeniedException異常,
1 @Component 2 public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler { 3 4 5@Override 6public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { 7httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); 8httpServletResponse.setContentType("application/json;charset=UTF-8"); 9PrintWriter writer = httpServletResponse.getWriter(); 10 11writer.print("許可權不足"); 12writer.flush(); 13} 14 }
至於哪種異常由哪個類處理,如果瞭解原始碼的都知道spring security有一個異常處理過濾器,名字為 ExceptionTranslationFilter,要想進一步瞭解的,可自行看原始碼,這裡提供一個個人認為寫的挺好的博文, 連結地址 ,這裡不多說廢話。
相信大家看完以上文章,對spring security應該有一個大致的瞭解,,這裡附上一個spring security請求經過的過濾器Filter,
執行順序從上到下。要想研究一波,大家可以先從DelegatingFilterProxy類及它的父類開始入手,一步一步debug下去,相信會有收穫的。 關於WebSecurityConfig 的配置情況,這裡也不多說,網上文章也挺多的。在這裡說下當初遇到的一個比較坑的坑
四、遇到的坑
當時場景是這樣的,因為專案採用的是前後端分離模式開發的,後端寫完程式碼需要部署到測試伺服器,供前端使用,採用的域名是https模式,使用了nginx程式碼模式,部署上去後。因為登入失敗後,spring security會請求到你指定的一個路徑,但此時問題出現了,程式碼部署上去了,測試了一個使用者名稱和密碼不正確的情況,結果發現跳轉後的host由https變成了http,例子:本來是請求https://abc.com/doLogin路徑,但是變成了htttp://abc.com/doLogin。這肯定是訪問不了,當時就有點懵了,後面經過分析發現,更改Nginx配置可以達到指定效果,在指定的location加入proxy_set_header X-Forwarded-Proto https,但是這樣侷限性也有,這樣做只能使用https進行訪問,所以就沒采用,後來就直接百度,百度了的結果大都是更改spring mvc 內部檢視解析器配置,如下面
1 <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> 2<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" /> 3<property name="prefix" value="/WEB-INF/" /> 4<property name="suffix" value=".jsp" /> 5<!-- 重點是下面配置,將其改為false --> 6<property name="redirectHttp10Compatible" value="false" /> 7 </bean>
不過redirect也提醒了我,這個情況由https 變成http 應該就是redirect搞的鬼。那如果將spring security內部由redirect改成forward呢,那情況又會怎樣,緊接著,又去看其原始碼,最後發現這樣一個類 LoginUrlAuthenticationEntryPoint負責spring security的重定向和轉發情況,在其 commence方法內進行操作,最後那肯定得試試,最後將該類的useForward屬性設定成了true,然後就完美解決。
--------------------------------------------------------------------------------------------------------------------------------------------------分界線--------------------------------------------------------------------------------------
以上就是全部內容,若有不足之處,還望指正,另外附上本文程式碼地址供大家參考 spring security demo