spring security入門demo

1、前言

  因項目須要引入spring security權限框架,而以前也沒接觸過這個一門,因而就花了點時間弄了個小demo出來,說實話,剛開始接觸這個確實有點懵,看網上資料寫的權限大都是靜態,即就是在配置文件或代碼裏面寫定角色,不能動態更改,我的感受這樣實際場景應該應用的很少,因而就進一步研究,整理出了一個能夠動態管理我的權限角色demo,其中可能有不少不足或之處,還望指正。本文經過spring boot集成spring security,處理方式沒有使用xml文件格式,而是用了註解。html

 2、表結構

接觸過權限這塊的,大都應該知道,最核心的有三張表(固然,若是牽涉業務複雜,可能不止)。前端

1、用戶表nginx

2、角色表git

3、菜單表(即權限表)github

剩餘還有兩張多對多的表。即用戶與角色,角色與菜單。以下圖web

3、spring security入口

因爲本文只是着重說spring security,關於spring boot一塊內容會直接帶過。如spring boot啓動類配置等。spring

首先會自定義一個類去實現WebSecurityConfigurerAdapter類。重寫其中幾個方法,代碼以下數據庫

 1 @Configuration
 2 public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 3 
 4     @Autowired
 5     @Qualifier(value = "userDetailServiceImpl")
 6     private UserDetailsService userDetailsService;
 7 
 8     @Autowired
 9     private LoginSuccessAuthenticationHandler successAuthenticationHandler;
10 
11     @Autowired
12     private LoginFailureAuthenticationHandler failureAuthenticationHandler;
13 
14     @Autowired
15     private AuthenticationAccessDeniedHandler accessDeniedHandler;
16 
17     @Autowired
18     private UrlAccessDecisionManager decisionManager;
19 
20     @Autowired
21     private UrlPathFilterInvocationSecurityMetadataSource urlPathFilterInvocationSecurityMetadataSource;
22 
23     @Autowired
24     private AuthenticationProvider authenticationProvider;
25 
26     @Autowired
27     private PasswordEncoder passwordEncoder;
28 
29     @Override
30     protected void configure(AuthenticationManagerBuilder auth) throws Exception {
31         auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
32         auth.authenticationProvider(authenticationProvider);
33     }
34 
35     @Override
36     public void configure(WebSecurity web) {
37         web.ignoring().antMatchers("/index.html","/favicon.ico");
38     }
39 
40     @Override
41     protected void configure(HttpSecurity http) throws Exception {
42         http.csrf().disable()
43                 .authorizeRequests()
44                 .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
45                     @Override
46                     public <O extends FilterSecurityInterceptor> O postProcess(O o) {
47                         o.setAccessDecisionManager(decisionManager);
48                         o.setSecurityMetadataSource(urlPathFilterInvocationSecurityMetadataSource);
49                         return 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     }

在這裏會介紹如下幾個類做用json

1、UserDetailsService
2、AuthenticationProvider
3、AuthenticationAccessDeniedHandler
4、UrlAccessDecisionManager
5、UrlPathFilterInvocationSecurityMetadataSource
至於LoginSuccessAuthenticationHandler、LoginFailureAuthenticationHandler就是用來處理登陸成功和登陸失敗狀況,這裏不作介紹

3.一、UserDetailService的做用

這個一個接口,一般咱們須要去實現它,做用主要是用來咱們和數據庫作交互用的。簡單來講,就是用戶名傳過來,這個類負責校驗用戶名是否存在等業務邏輯。後端

 1 @Component
 2 public class UserDetailServiceImpl implements UserDetailsService {
 3 
 4     @Autowired
 5     private SysUserDAO userDAO;
 6 
 7     @Autowired
 8     private PasswordEncoder passwordEncoder;
 9 
10 
11     @Override
12     public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
13         SysUser sysUser = userDAO.findByUsername(s);
14         if (sysUser == null){
15             throw new UsernameNotFoundException("用戶不存在");
16         }
17         String pwd = passwordEncoder.encode(sysUser.getPassword());
18         System.out.println(pwd);
19         return new User(sysUser.getUsername(),pwd,getRoles(sysUser.getRoles()));
20     }
21 
22     private Collection<GrantedAuthority> getRoles(List<SysRole> roles){
23         List<GrantedAuthority> list = new ArrayList<>();
24         for (SysRole role : roles){
25             SimpleGrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
26             list.add(grantedAuthority);
27         }
28         return list;
29     }
30 }

 

代碼比較簡單,值得注意的是sercurity裏的User對象,它的一個構造函數有是哪一個參數值,第一個和第二個是用戶名和密碼,密碼做用就是後面用來校驗前端傳過來的密碼正確性。稍後會講到。至於第三個參數就是當前用戶所擁有的角色,做用就是在當前端請求一個接口的時候,會判斷這個接口所擁有的權限和該用戶全部的權限有重合,簡單來講就是該用戶是否擁有該接口權限。這裏也就實現了一個角色能夠動態修改的功能。因其實從數據庫查詢出來。

3.二、AuthenticationProvider

它也是一個接口,它的做用是用來校驗用戶密碼等功能,固然如短信驗證或要第三方驗證,也能夠實現這個接口,在本文中是用密碼校驗。前面也說到userDetailService會傳一個用戶的基本信息。它的主要做用就是爲該接口服務的。

 1 @Component
 2 public class LoginAuthenticationProvider implements AuthenticationProvider {
 3 
 4     @Autowired
 5     private UserDetailsService userDetailsService;
 6 
 7     @Override
 8     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 9         // 獲取表單用戶名
10         String username = (String) authentication.getPrincipal();
11         // 獲取表單用戶填寫的密碼
12         String password = (String) authentication.getCredentials();
13 
14         UserDetails userDetails = userDetailsService.loadUserByUsername(username);
15 
16         String password1 = userDetails.getPassword();
17         if (!Objects.equals(password,password1)){
18             throw new BadCredentialsException("用戶名或密碼不正確");
19         }
20 
21         return new UsernamePasswordAuthenticationToken(username,password,userDetails.getAuthorities());
22     }
23 
24     @Override
25     public boolean supports(Class<?> aClass) {
26         return true;
27     }
28 }

 

值得注意的是若是驗證經過會返回一個UsernamePasswordAuthenticationToken對象,它的做用就是標誌着此用戶已經過登陸驗證,若是沒經過,則spring security會捕捉如代碼18行的異常,而後再包裝一個匿名的token,即AnonymousAuthenticationToken,此token即表明用戶未登陸。兩個接口主要服務於用戶登陸這塊。接下來的三個是服務於權限校驗。即接口驗證

3.三、UrlPathFilterInvocationSecurityMetadataSource

 它的做用是用來處理當前用戶是否擁有此接口的權限。

 1 @Component
 2 public class UrlPathFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
 3 
 4 
 5     @Autowired
 6     private SysMenuDAO sysMenuDAO;
 7 
 8     private AntPathMatcher antPathMatcher = new AntPathMatcher();
 9 
10     @Override
11     public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
12         FilterInvocation filterInvocation = (FilterInvocation) object;
13         String requestUrl = filterInvocation.getRequestUrl();
14         // 由於菜單通常隨着開發完成,變更不大,此處可使用緩存,這裏爲了演示,就直接查庫,菜單對應角色須要動態情緩存,如變動菜單和角色關係,需清除緩存
15         List<SysMenu> all = sysMenuDAO.findAll();
16         for (SysMenu menu : all) {
17             if (menu.getRoles().size() != 0 && antPathMatcher.match(menu.getUrlPath(), requestUrl)) {
18                 List<SysRole> roles = menu.getRoles();
19                 int size = roles.size();
20                 String[] values = new String[size];
21                 for (int i = 0; i < size; i++) {
22                     values[i] = roles.get(i).getRoleName();
23                 }
24                 return SecurityConfig.createList(values);
25             }
26         }
27         return SecurityConfig.createList("ROLE_LOGIN");
28     }
29 
30     @Override
31     public Collection<ConfigAttribute> getAllConfigAttributes() {
32         return null;
33     }
34 
35     @Override
36     public boolean supports(Class<?> clazz) {
37         return true;
38     }
39 }

 

從代碼就能夠看出16行的for循環就是獲取當前請求接口鎖須要的權限,這裏使用spring security的路徑匹配類。若是該接口·沒有權限,這裏返回一個標誌如ROLE_LOGIN,固然若是須要其餘標誌能夠自行定義,這裏爲了簡便,就用了這個。

3.四、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
13     public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
14         Iterator<ConfigAttribute> iterator = configAttributes.iterator();
15         while (iterator.hasNext()) {
16             ConfigAttribute ca = iterator.next();
17             //當前請求須要的權限
18             String needRole = ca.getAttribute();
19             if ("ROLE_LOGIN".equals(needRole)) {
20                 // 即匿名用戶/未登陸,若是用戶登陸成功。那麼authententication就是前面提到的UsernamePasswordAuthententicationToken類
21                 if (authentication instanceof AnonymousAuthenticationToken) {
22                     throw new BadCredentialsException("未登陸");
23                 } else {// 登陸但不具備此路徑權限,即前面3.3提到的ROLE_LOGIN,接口沒有角色對應,主要用戶已經登陸成功
24                     break;
25                 }
26             }
27             //當前用戶所具備的權限
28             Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
29             for (GrantedAuthority authority : authorities) {
30                 if (authority.getAuthority().equals(needRole)) {
31                     return;
32                 }
33             }
34         }
35         throw new AccessDeniedException("權限不足!");
36     }
37 
38     @Override
39     public boolean supports(ConfigAttribute attribute) {
40         return true;
41     }
42 
43     @Override
44     public boolean supports(Class<?> clazz) {
45         return true;
46     }
47 }

3.五、AuthenticationAccessDeniedHandler

這個類就是用來接收上面拋出的accessDeniedException異常,

 1 @Component
 2 public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
 3 
 4 
 5     @Override
 6     public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
 7         httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
 8         httpServletResponse.setContentType("application/json;charset=UTF-8");
 9         PrintWriter writer = httpServletResponse.getWriter();
10 
11         writer.print("權限不足");
12         writer.flush();
13     }
14 }

 

至於哪一種異常由哪一個類處理,若是瞭解源碼的都知道spring security有一個異常處理過濾器,名字爲ExceptionTranslationFilter,要想進一步瞭解的,可自行看源碼,這裏提供一個我的認爲寫的挺好的博文,連接地址,這裏很少說廢話。

相信你們看完以上文章,對spring security應該有一個大體的瞭解,,這裏附上一個spring security請求通過的過濾器Filter,

執行順序從上到下。要想研究一波,你們能夠先從DelegatingFilterProxy類及它的父類開始入手,一步一步debug下去,相信會有收穫的。關於WebSecurityConfig 的配置狀況,這裏也很少說,網上文章也挺多的。在這裏說下當初遇到的一個比較坑的坑

4、遇到的坑

當時場景是這樣的,由於項目採用的是先後端分離模式開發的,後端寫完代碼須要部署到測試服務器,供前端使用,採用的域名是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

相關文章
相關標籤/搜索