看完這一篇,你就對 Spring Security 略窺門徑了

看完這一篇,你就對 Spring Security 略窺門徑了 | 原力計劃

作者 | BoCong-Deng

來源 | CSDN 博客,責編 | 夕顏

頭圖 | CSDN 下載自東方 IC

出品 | CSDN(ID:CSDNnews)

寫在前面

開發Web應用,對頁面的安全控制通常是必須的。比如:對於沒有訪問權限的用戶需要轉到登錄表單頁面。要實現訪問控制的方法多種多樣,可以通過Aop、攔截器實現,也可以通過框架實現,例如:Apache Shiro、Spring Security。我們這裡要講的Spring Security 就是一個Spring生態中關於安全方面的框架。它能夠為基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案。

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

默認認證用戶名密碼

項目pom.xml添加spring-boot-starter-security依賴

<code>1<dependency>
2 <groupid>org.springframework.boot/<groupid>
3 <artifactid>spring-boot-starter-security/<artifactid>
4/<dependency>
/<code>

重啟你的應用。再次打開頁面,你講看到一個登錄頁面

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

既然跳到了登錄頁面,那麼這個時候我們就會想,這個登錄的用戶名以及密碼是什麼呢?讓我們來從SpringBoot源碼尋找一下。你搜一下輸出日誌,會看到下面一段輸出:

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

這段日誌是UserDetailsServiceAutoConfiguration類裡面的如下方法輸出的:

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

通過上面的這個類,我們可以看出,是SecurityProperties這個Bean管理了用戶名和密碼。在SecurityProperties裡面的一個內部靜態類User類裡面,管理了默認的認證的用戶名與密碼。代碼如下

<code> 1@ConfigurationProperties(
2 prefix = "spring.security"
3)
4public class SecurityProperties {
5 public static final int BASIC_AUTH_ORDER = 2147483642;
6 public static final int IGNORED_ORDER = -2147483648;
7 public static final int DEFAULT_FILTER_ORDER = -100;
8 private final SecurityProperties.Filter filter = new SecurityProperties.Filter;
9 private SecurityProperties.User user = new SecurityProperties.User;
10
11 public SecurityProperties {
12 }
13
14 public SecurityProperties.User getUser {
15 return this.user;
16 }
17
18 public SecurityProperties.Filter getFilter {
19 return this.filter;
20 }
21
22 public static class User {
23 private String name = "user";
24 private String password = UUID.randomUUID.toString;
25 private List<string> roles = new ArrayList;
26 private boolean passwordGenerated = true;
27
28 public User {
29 }
30
31 public String getName {
32 return this.name;
33 }
34
35 public void setName(String name) {
36 this.name = name;
37 }
38
39 public String getPassword {
40 return this.password;
41 }
42

43 public void setPassword(String password) {
44 if (StringUtils.hasLength(password)) {
45 this.passwordGenerated = false;
46 this.password = password;
47 }
48 }
49
50 public List<string> getRoles {
51 return this.roles;
52 }
53
54 public void setRoles(List<string> roles) {
55 this.roles = new ArrayList(roles);
56 }
57
58 public boolean isPasswordGenerated {
59 return this.passwordGenerated;
60 }
61 }
62
63 public static class Filter {
64 private int order = -100;
65 private Set<dispatchertype> dispatcherTypes;
66
67 public Filter {
68 this.dispatcherTypes = new HashSet(Arrays.asList(DispatcherType.ASYNC, DispatcherType.ERROR, DispatcherType.REQUEST));
69 }
70
71 public int getOrder {
72 return this.order;
73 }
74
75 public void setOrder(int order) {
76 this.order = order;
77 }
78
79 public Set<dispatchertype> getDispatcherTypes {
80 return this.dispatcherTypes;
81 }
82
83 public void setDispatcherTypes(Set<dispatchertype> dispatcherTypes) {
84 this.dispatcherTypes = dispatcherTypes;
85 }
86 }
87}
/<dispatchertype>/<dispatchertype>/<dispatchertype>/<string>/<string>/<string>/<code>

綜上所述,security默認的用戶名是user, 默認密碼是應用啟動的時候,通過UUID算法隨機生成的,默認的role是"USER"。當然,如果我們想簡單改一下這個用戶名密碼,可以在application.properties配置你的用戶名密碼,例如

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

當然這只是一個初級的配置,更復雜的配置,可以分不用角色,在控制範圍上,能夠攔截到方法級別的權限控制。

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

內存用戶名密碼認證

在上面的內容,我們什麼都沒做,就添加了spring-boot-starter-security依賴,整個應用就有了默認的認證安全機制。下面,我們來定製用戶名密碼。寫一個繼承了 WebSecurityConfigurerAdapter的配置類,具體內容如下

<code> 1import org.springframework.context.annotation.Configuration;
2import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
3import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
4import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
6import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
7
8@Configuration
9@EnableWebSecurity
10@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
11public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
12 @Override
13 protected void configure(HttpSecurity http) throws Exception {
14 super.configure(http);
15 }
16
17 @Override
18 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
19 auth.inMemoryAuthentication
20 .passwordEncoder(new BCryptPasswordEncoder)
21 .withUser("admin")
22 .password(new BCryptPasswordEncoder.encode("1234567"))
23 .roles("USER");
24 }
25}
/<code>

這裡對上面的代碼進行簡要說明:

  • Spring security 5.0中新增了多種加密方式,也改變了默認的密碼格式。需要修改一下configure中的代碼,我們要將前端傳過來的密碼進行某種方式加密,Spring Security 官方推薦的是使用bcrypt加密方式。inMemoryAuthentication.passwordEncoder(new BCryptPasswordEncoder),這相當於登陸時用BCrypt加密方式對用戶密碼進行處理。以前的".password("123")" 變成了 “.password(new BCryptPasswordEncoder.encode("123"))”,這相當於對內存中的密碼進行Bcrypt編碼加密。如果比對時一致,說明密碼正確,才允許登陸。

  • 通過 @EnableWebSecurity註解開啟Spring Security的功能。使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,可以開啟security的註解,我們可以在需要控制權限的方法上面使用@PreAuthorize,@PreFilter這些註解。

  • 繼承 WebSecurityConfigurerAdapter 類,並重寫它的方法來設置一些web安全的細節。我們結合@EnableWebSecurity註解和繼承WebSecurityConfigurerAdapter,來給我們的系統加上基於web的安全機制。

  • 在configure(HttpSecurity http)方法裡面,我們進入到源碼中,就會看到默認的認證代碼是:

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

從方法名我們基本可以看懂這些方法的功能。上面的那個默認的登錄頁面,就是SpringBoot默認的用戶名密碼認證的login頁面。我們使用SpringBoot默認的配置super.configure(http),它通過 authorizeRequests 定義哪些URL需要被保護、哪些不需要被保護。默認配置是所有訪問頁面都需要認證,才可以訪問。

  • 通過 formLogin 定義當需要用戶登錄時候,轉到的登錄頁面。

  • configureGlobal(AuthenticationManagerBuilder auth) 方法,在內存中創建了一個用戶,該用戶的名稱為root,密碼為root,用戶角色為USER。這個默認的登錄頁面是怎麼冒出來的呢?是的,SpringBoot內置的,SpringBoot甚至給我們做好了一個極簡的登錄頁面。這個登錄頁面是通過Filter實現的。具體的實現類是org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter。同時,這個DefaultLoginPageGeneratingFilter也是SpringBoot的默認內置的Filter。

輸入用戶名,密碼,點擊Login。不過,我們發現,SpringBoot應用的啟動日誌還是打印瞭如下一段:

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

但實際上,已經使用了我們定製的用戶名密碼了。如果我們要配置多個用戶,多個角色,可參考使用如下示例的代碼:

<code> 1@Override
2 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
3 auth.inMemoryAuthentication
4 .passwordEncoder(new BCryptPasswordEncoder)
5 .withUser("admin")
6 .password(new BCryptPasswordEncoder.encode("1234567"))
7 .roles("USER")
8 .and
9 .withUser("admin1")
10 .password(new BCryptPasswordEncoder.encode("123"))
11 .roles("ADMIN", "USER");
12 }
/<code>
看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

角色權限控制

當我們的系統功能模塊當需求發展到一定程度時,會不同的用戶,不同角色使用我們的系統。這樣就要求我們的系統可以做到,能夠對不同的系統功能模塊,開放給對應的擁有其訪問權限的用戶使用。Spring Security提供了Spring EL表達式,允許我們在定義URL路徑訪問(@RequestMapping)的方法上面添加註解,來控制訪問權限。在標註訪問權限時,根據對應的表達式返回結果,控制訪問權限:

<code>1true,表示有權限
2fasle,表示無權限
/<code>

Spring Security可用表達式對象的基類是SecurityExpressionRoot。

<code> 1public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
2 protected final Authentication authentication;
3 private AuthenticationTrustResolver trustResolver;
4 private RoleHierarchy roleHierarchy;
5 private Set<string> roles;
6 private String defaultRolePrefix = "ROLE_";
7 public final boolean permitAll = true;
8 public final boolean denyAll = false;
9 private PermissionEvaluator permissionEvaluator;
10 public final String read = "read";
11 public final String write = "write";
12 public final String create = "create";
13 public final String delete = "delete";
14 public final String admin = "administration";
15
16 public SecurityExpressionRoot(Authentication authentication) {
17 if (authentication == ) {
18 throw new IllegalArgumentException("Authentication object cannot be ");
19 } else {
20 this.authentication = authentication;
21 }
22 }
23
24 public final boolean hasAuthority(String authority) {
25 return this.hasAnyAuthority(authority);
26 }
27
28 public final boolean hasAnyAuthority(String... authorities) {
29 return this.hasAnyAuthorityName((String), authorities);
30 }
31
32 public final boolean hasRole(String role) {
33 return this.hasAnyRole(role);
34 }
35
36 public final boolean hasAnyRole(String... roles) {
37 return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
38 }
39
40 private boolean hasAnyAuthorityName(String prefix, String... roles) {
41 Set<string> roleSet = this.getAuthoritySet;

42 String var4 = roles;
43 int var5 = roles.length;
44
45 for(int var6 = 0; var6 < var5; ++var6) {
46 String role = var4[var6];
47 String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
48 if (roleSet.contains(defaultedRole)) {
49 return true;
50 }
51 }
52
53 return false;
54 }
55
56 public final Authentication getAuthentication {
57 return this.authentication;
58 }
59
60 public final boolean permitAll {
61 return true;
62 }
63
64 public final boolean denyAll {
65 return false;
66 }
67
68 public final boolean isAnonymous {
69 return this.trustResolver.isAnonymous(this.authentication);
70 }
71
72 public final boolean isAuthenticated {
73 return !this.isAnonymous;
74 }
75
76 public final boolean isRememberMe {
77 return this.trustResolver.isRememberMe(this.authentication);
78 }
79
80 public final boolean isFullyAuthenticated {
81 return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication);
82 }
83
84 public Object getPrincipal {
85 return this.authentication.getPrincipal;
86 }
87
88 public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
89 this.trustResolver = trustResolver;
90 }
91

92 public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
93 this.roleHierarchy = roleHierarchy;
94 }
95
96 public void setDefaultRolePrefix(String defaultRolePrefix) {
97 this.defaultRolePrefix = defaultRolePrefix;
98 }
99
100 private Set<string> getAuthoritySet {
101 if (this.roles == ) {
102 Collection extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities;
103 if (this.roleHierarchy != ) {
104 userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
105 }
106
107 this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
108 }
109
110 return this.roles;
111 }
112
113 public boolean hasPermission(Object target, Object permission) {
114 return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
115 }
116
117 public boolean hasPermission(Object targetId, String targetType, Object permission) {
118 return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
119 }
120
121 public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
122 this.permissionEvaluator = permissionEvaluator;
123 }
124
125 private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
126 if (role == ) {
127 return role;
128 } else if (defaultRolePrefix != && defaultRolePrefix.length != 0) {
129 return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role;
130 } else {
131 return role;
132 }
133 }
134}
/<string>/<string>/<string>/<code>

通過閱讀源碼,我們可以更加深刻的理解其EL寫法,並在寫代碼的時候正確的使用。變量defaultRolePrefix硬編碼約定了role的前綴是"ROLE_"。同時,我們可以看出hasRole跟hasAnyRole是一樣的。hasAnyRole是調用的hasAnyAuthorityName(defaultRolePrefix, roles)。所以,我們在學習一個框架或者一門技術的時候,最準確的就是源碼。通過源碼,我們可以更好更深入的理解技術的本質。

SecurityExpressionRoot為我們提供的使用Spring EL表達式總結如下:

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

在Controller方法上添加@PreAuthorize這個註解,value="hasRole('ADMIN')")是Spring-EL expression,當表達式值為true,標識這個方法可以被調用。如果表達式值是false,標識此方法無權限訪問。

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

在Spring Security裡獲取當前登錄認證通過的用戶信息

如果我們想要在前端頁面顯示當前登錄的用戶怎麼辦呢?在在Spring Security裡面怎樣獲取當前登錄認證通過的用戶信息?下面我們就來探討這個問題。其實很好辦。我們添加一個LoginFilter,默認攔截所有請求,把當前登錄的用戶放到系統session中即可。在Spring Security中,用戶信息保存在SecurityContextHolder中。Spring Security使用一個Authentication對象來持有所有系統的安全認證相關的信息。這個信息的內容格式如下:

<code> 1{
2 "accountNonExpired":true,
3 "accountNonLocked":true,
4 "authorities":[{
5 "authority":"ROLE_ADMIN"
6 },{
7 "authority":"ROLE_USER"
8 }],
9 "credentialsNonExpired":true,
10 "enabled":true,
11 "username":"root"
12}
/<code>

這個Authentication對象信息其實就是User實體的信息,類似如下(當然,密碼沒放進來)。

<code> 1public class User implements UserDetails, CredentialsContainer {
2 private String password;
3 private final String username;
4 private final Set<grantedauthority> authorities;
5 private final boolean accountNonExpired;
6 private final boolean accountNonLocked;
7 private final boolean credentialsNonExpired;
8 private final boolean enabled;
9 ....

10}
/<grantedauthority>/<code>

我們可以使用下面的代碼(Java)獲得當前身份驗證的用戶的名稱:

<code>1Object principal = SecurityContextHolder.getContext.getAuthentication.getPrincipal;
2
3if (principal instanceof UserDetails) {
4 String username = ((UserDetails)principal).getUsername;
5} else {
6 String username = principal.toString;
7}
/<code>

通過調用getContext返回的對象是SecurityContext的實例對象,該實例對象保存在ThreadLocal線程本地存儲中。使用Spring Security框架,通常的認證機制都是返回UserDetails實例,通過如上這種方式,我們就可以拿到認證登錄的用戶信息。

看完这一篇,你就对 Spring Security 略窥门径了 | 原力计划

用數據庫存儲用戶和角色,實現安全認證

很多時候,我們需要的是實現一個用數據庫存儲用戶和角色,實現系統的安全認證。為了簡化講解,本例中在權限角色上,我們簡單設計兩個用戶角色:USER,ADMIN。我們設計頁面的權限如下:

  • 首頁/ : 所有人可訪問

  • 登錄頁 /login: 所有人可訪問

  • 普通用戶權限頁 /httpapi, /httpsuite: 登錄後的用戶都可訪問

  • 管理員權限頁 /httpreport :僅管理員可訪問

  • 無權限提醒頁:當一個用戶訪問了其沒有權限的頁面,我們使用全局統一的異常處理頁面提示。

配置Spring Security

我們首先使用Spring Security幫我們做登錄、登出的處理,以及當用戶未登錄時只能訪問: http://localhost:8080/ 以及 http://localhost:8080/login 兩個頁面。同樣的,我們要寫一個繼承WebSecurityConfigurerAdapter的配置類:

<code> 1import com.springboot.in.action.service.LightSwordUserDetailService;
2import org.springframework.context.annotation.Bean;
3import org.springframework.context.annotation.Configuration;
4import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
5import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
6import org.springframework.security.config.annotation.web.builders.HttpSecurity;
7import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
8import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
9import org.springframework.security.core.userdetails.UserDetailsService;
10
11/**
12 * Created by jack on 2017/4/27.
13 */
14
15@Configuration
16@EnableWebSecurity
17@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
18//使用@EnableGlobalMethodSecurity(prePostEnabled = true)
19// 這個註解,可以開啟security的註解,我們可以在需要控制權限的方法上面使用@PreAuthorize,@PreFilter這些註解。
20public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
21 @Override
22 @Bean
23 public UserDetailsService userDetailsService { //覆蓋寫userDetailsService方法 (1)
24 return new AdminUserDetailService;
25
26 }
27
28 /**
29 * If subclassed this will potentially override subclass configure(HttpSecurity)
30 *
31 * @param http
32 * @throws Exception
33 */
34 @Override
35 protected void configure(HttpSecurity http) throws Exception {
36 //super.configure(http);
37 http.csrf.disable;

38
39 http.authorizeRequests
40 .antMatchers("/").permitAll
41 .antMatchers("/amchart/**",
42 "/bootstrap/**",
43 "/build/**",
44 "/css/**",
45 "/dist/**",
46 "/documentation/**",
47 "/fonts/**",
48 "/js/**",
49 "/pages/**",
50 "/plugins/**"
51 ).permitAll //默認不攔截靜態資源的url pattern (2)
52 .anyRequest.authenticated.and
53 .formLogin.loginPage("/login")// 登錄url請求路徑 (3)
54 .defaultSuccessUrl("/httpapi").permitAll.and // 登錄成功跳轉路徑url(4)
55 .logout.permitAll;
56
57 http.logout.logoutSuccessUrl("/"); // 退出默認跳轉頁面 (5)
58
59 }
60
61 @Override
62 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
63 //AuthenticationManager使用我們的 Service來獲取用戶信息,Service可以自己寫,其實就是簡單的讀取數據庫的操作
64 auth.userDetailsService; // (6)
65 }
66
67}
/<code>

上面的代碼只做了基本的配置,其中:

  • 覆蓋寫userDetailsService方法,具體的AdminUserDetailsService實現類,就是之前說的獲取用戶信息的service層類。

  • 默認不攔截靜態資源的url pattern。我們也可以用下面的WebSecurity這個方式跳過靜態資源的認證。

<code>1public void configure(WebSecurity web) throws Exception {
2 web
3 .ignoring
4 .antMatchers("/resourcesDir/**");
5}
/<code>
  • 跳轉登錄頁面url請求路徑為/login,我們需要定義一個Controller把路徑映射到login.html。

  • 登錄成功後跳轉的路徑為/httpapi

  • 退出後跳轉到的url為/

  • 認證鑑權信息的Bean,採用我們自定義的從數據庫中獲取用戶信息的AdminUserDetailService類。

我們同樣使用@EnableGlobalMethodSecurity(prePostEnabled = true)這個註解,開啟security的註解,這樣我們可以在需要控制權限的方法上面使用@PreAuthorize,@PreFilter這些註解。

用戶退出

我們在configure(HttpSecurity http)方法裡面定義了任何權限都允許退出,當然SpringBoot集成Security的默認退出請求是/logout

<code>1http.logout.logoutSuccessUrl("/"); // 退出默認跳轉頁面 (4)
/<code>

配置錯誤處理頁面

訪問發生錯誤時,跳轉到系統統一異常處理頁面。我們首先添加一個GlobalExceptionHandlerAdvice,使用@ControllerAdvice註解:

<code> 1import org.springframework.web.bind.annotation.{ControllerAdvice, ExceptionHandler}
2import org.springframework.web.context.request.WebRequest
3import org.springframework.web.servlet.ModelAndView
4
5/**
6 * Created by jack on 2017/4/27.
7 */
8@ControllerAdvice
9class GlobalExceptionHandlerAdvice {
10 @ExceptionHandler(value = Exception.class)//表示捕捉到所有的異常,你也可以捕捉一個你自定義的異常
11 public ModelAndView exception(Exception exception, WebRequest request){
12 ModelAndView modelAndView = new ModelAndView("/error");
13 modelAndView.addObject("errorMessage", exception.getMessage);
14 modelAndView.addObject("stackTrace", exception.getStackTrace);
15 return modelAndView;
16 }
17}
/<code>

其中,@ExceptionHandler(value = Exception.class),表示捕捉到所有的異常,這裡你也可以捕捉一個你自定義的異常。比如說,針對安全認證的Exception,我們可以單獨定義處理。此處不再贅述。

原文鏈接:

https://blog.csdn.net/DBC_121/article/details/104740273


分享到:


相關文章: