7000 字 Spring Security 淺析!!!

流量如水,文章就是一個瓶子,標題是瓶口,內容是瓶身。 瓶子裝水有多快依賴瓶口大小,瓶子裝水有多少依賴瓶身大小。

Spring Security 是什麼?

Spring Security 是 Spring 家族中一個安全管理框架,實際上,在 Spring Boot 出現之前,Spring Security 就已經發展了好多年了,但是使用的並不多,安全管理這塊,一直主打的是 Shiro 。

Spring Security 與 Shiro 的區別?

相對於 Shiro 來說,在經典的 SSM/SSH 框架中整合 Spring Security 都是比較繁瑣,雖然 Spring Security 功能比 Shiro 強大,但是因爲配置比較繁瑣,使用的反而沒有 Shiro 多。
兩者不同之處:
1. Spring Security 功能比 Shiro 更加豐富一些;
2. Spring Security 上手複雜;Shiro 上手簡單;
3. Spring Security 依賴 Spring 容器;Shiro 依賴性低,不需要任何框架和容器;
自從有了 Spring Boot 之後,Spring Boot 對於 Spring Security 提供了自動化配置方案,可以零配置使用 Spring Security ,下面看一下具體使用吧!

Spring Security 初體驗?

1. 準備環境以及工具
  • JDK 8
  • IDEA
2. 創建項目

在 Spring Boot 中使用 Spring Security 非常容易,只需要引入對應依賴即可:

在這裏插入圖片描述
pom.xml 中的 Spring Security 依賴:

<dependency>           
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

我們創建一個測試接口:

@RestController
public class HelloController {
	@GetMapping("/hello")
    public String hello() {
        return "hello security!";
    }
}

啓動項目,通過 URL 訪問 /hello 接口,需要登陸之後才能訪問。
默認的賬戶是:user
默認的密碼是隨機生成的,這裏我們看一下控制檯隨機生成的密碼是多少。
在這裏插入圖片描述
在這裏插入圖片描述

  • 好現在我們賬號密碼都知道是多少了,這裏我們去測試登陸。
    在這裏插入圖片描述

自定義用戶名或密碼

默認情況下,登陸的用戶名是 user,密碼則是項目啓動時隨機生成的字符串,可以從啓動的控制檯日誌中看到默認密碼,這個隨機生成的密碼,每次啓動都會變,對登陸的用戶名/密碼進行配置,有三種不同的方式:

  • 在 application.properties 中進行配置
  • 通過 Java 代碼配置到內存中
  • 通過 Java 從數據庫中加載
1. 在配置文件中配置用戶名/密碼

可以直接在 application.properties 中進行配置用戶的基本信息:
這裏配置完成後,重啓項目,控制檯就不會隨機生成密碼了,你就可以使用這裏配置的用戶名和密碼登陸了。因爲這裏測試同上,我們就簡略了!

spring.security.user.name=javaboy
spring.security.user.password=123
spring.security.user.roles=admin
2. 通過 Java 配置用戶名/密碼

第二種情況:首先我們需要創建一個 Spring Security 的配置類,繼承 WebSecurityConfigurerAdapter 類,代碼如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
    PasswordEncoder passwordEncoder() {
    	// 表示對密碼進行加密加鹽
        return new BCryptPasswordEncoder(); 
    }

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                // 這行表示配置了用戶 javaboy 密碼爲 123 角色爲 admin
                .withUser("javaboy").password("$2a$10$3pOk/IanPEdCev05Wmew/uTOj96b3KtDc11nCTOHQdjITV0WA4rQW").roles("admin") 
                .and()
                // 這行表示配置了用戶 紅顏禍水 密碼爲 123 角色爲 user
                .withUser("紅顏禍水").password("$2a$10$uNB.x3J3ebn8nirQkRdljO5ZtNQMODKghfNA7J/W07XbrmlBdIzr.").roles("user"); 
    }
}

這裏我們在 configure 方法中配置了兩個用戶,用戶的密碼都是加密之後的字符串(明文是 123),從 Spring 5 開始,強制要求密碼要加密,如果非不想加密,可以使用一個早已過期的 PasswordEncoder de 實例 NoOpPasswordEcoder ,但是不建議這麼做,因爲不安全。
NoOpPasswordEcoder 如何使用代碼如下:

	@Bean
    PasswordEncoder passwordEncoder() {
    	// 表示不對密碼進行加密操作
        return NoOpPasswordEncoder.getInstance();
    }

如何實現BCryptPasswordEncoder 對密碼進行加密加鹽
首先在項目測試類中添加如下代碼,並輸入到控制檯:

@Test
    void contextLoads() {
        for (int i = 0; i < 10; i++) {
        	// 這裏創建 BCryptPasswordEncoder 的實例
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            // 對明文密碼加密加鹽並輸入
            System.out.println(encoder.encode("123"));
        }
    }

控制檯結果如下圖所示:
在這裏插入圖片描述

登陸表單配置

對於登陸接口,登陸成功後的響應,登陸失敗後的響應,我們都可以在 WebSecurityConfigurerAdapter 的實現類中進行配置,例如下面這樣:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Bean
    PasswordEncoder passwordEncoder() {
    	// 表示對密碼進行加密加鹽
        return new BCryptPasswordEncoder(); 
    }

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                // 這行表示配置了用戶 javaboy 密碼爲 123 角色爲 admin
                .withUser("javaboy").password("$2a$10$3pOk/IanPEdCev05Wmew/uTOj96b3KtDc11nCTOHQdjITV0WA4rQW").roles("admin") 
                .and()
                // 這行表示配置了用戶 紅顏禍水 密碼爲 123 角色爲 user
                .withUser("紅顏禍水").password("$2a$10$uNB.x3J3ebn8nirQkRdljO5ZtNQMODKghfNA7J/W07XbrmlBdIzr.").roles("user"); 
    }

	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        		// 這行表示訪問 /admin/** 需要具備 admin 角色權限
                .antMatchers("/admin/**").hasRole("admin") 
                // 這行表示訪問 /user/** 需要具備 admin 或者 user 角色權限
                .antMatchers("/user/**").hasAnyRole("admin","user") 
                // 這行表示剩下的其他請求只要登陸成功就能訪問
                .anyRequest().authenticated()   
                .and()
                // 這行表示配置表單登陸
                .formLogin()   
                // 這行表示處理表單登陸的 URL 爲 doLogin 
                .loginProcessingUrl("/doLogin") 
                // 這行表示配置 Security 默認的登陸頁面
                .loginPage("/login")  
                // 自定義登陸名參數爲 uname   
                .usernameParameter("uname") 
                // 自定義登陸密碼參數爲 passwd    
                .passwordParameter("passwd")   
                // 登陸成功後的處理 
                .successHandler(new AuthenticationSuccessHandler() {    
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");  // 設置響應頭爲 json 格式
                        PrintWriter out = resp.getWriter();
                        Map<String,Object> map = new HashMap<>();
                        map.put("status",200);  // 登陸成功響應碼 200
                        map.put("msg",authentication.getPrincipal());   // 將登陸成功後的用戶信息返回
                        out.write(new ObjectMapper().writeValueAsString(map) );
                        out.flush();
                        out.close();
                    }
                })
                // 登陸失敗後的處理
                .failureHandler(new AuthenticationFailureHandler() {    
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest req,
                                                        HttpServletResponse resp,
                                                        AuthenticationException e) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");  // 設置響應頭爲 json 格式
                        PrintWriter out = resp.getWriter();
                        Map<String,Object> map = new HashMap<>();
                        map.put("status",401);  // 登陸失敗響應碼 401
                        if (e instanceof LockedException) {
                            map.put("msg","賬戶被鎖定,登陸失敗!");
                        } else if (e instanceof BadCredentialsException) {
                            map.put("msg","用戶名或密碼輸入錯誤,登陸失敗!");
                        } else if (e instanceof DisabledException) {
                            map.put("msg","賬戶被禁用,登陸失敗!");
                        } else if (e instanceof AccountExpiredException) {
                            map.put("msg","賬戶過期,登陸失敗!");
                        } else if (e instanceof CredentialsExpiredException) {
                            map.put("msg","密碼過期,登陸失敗!");
                        } else {
                            map.put("msg","登陸失敗!");
                        }
                        out.write(new ObjectMapper().writeValueAsString(map) );
                        out.flush();
                        out.close();
                    }
                })
                // 這行表示只要跟登陸相關的接口直接通過
                .permitAll() 
                .and()
                // 註銷
                .logout()   
                // 處理註銷的請求地址
                .logoutUrl("/logout") 
                // 註銷後的處理
                .logoutSuccessHandler(new LogoutSuccessHandler() {     
                    @Override
                    public void onLogoutSuccess(HttpServletRequest req,
                                                HttpServletResponse resp,
                                                Authentication authentication) throws IOException, ServletException {
                        resp.setContentType("application/json;charset=utf-8");  // 設置響應頭爲 json 格式
                        PrintWriter out = resp.getWriter();
                        Map<String,Object> map = new HashMap<>();
                        map.put("status",200);  // 登陸成功響應碼 200
                        map.put("msg","註銷成功!");   // 將登陸成功後的用戶信息返回
                        out.write(new ObjectMapper().writeValueAsString(map) );
                        out.flush();
                        out.close();
                    }
                })
                .and()
                // 這行表示關閉 csrf 攻擊,因爲使用 Postman 工具測試,Security 框架會默認以爲 Postman 的請求帶有 csrf 攻擊
                .csrf().disable(); 
    }
}

我們可以在 successHandler 方法中,配置登陸成功的回調,如果是前後端分離的開發的話,登陸成功後直接返回 JSON 即可,同理,failureHandler 方法中配置登陸失敗的回調, logoutSuccessHandler 中配置註銷成功的回調。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章