完全弄清SpringSecurity登陸原理及開發步驟

SpringBoot+Vue之SpringSecurity登陸與受權(一)

工具:idea2018,springboot 2.1.4,springsecurity 5.1.5html

簡介

SpringSecurity是Spring下的一個安全框架,與shiro 相似,通常用於用戶認證(Authentication)和用戶受權(Authorization)兩個部分,常與與SpringBoot相整合。前端

開發步驟

便於理解,下一節再使用先後端分離,並引入數據庫用戶和角色信息java

測試登陸

1 導入依賴

(pom.xml)web

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.1</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
複製代碼

2 編寫測試方法

(controller.UserController)算法

@Controller
public class UserController {

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return "hello controller";
    }
}    
複製代碼

3 測試

啓動項目,瀏覽器訪問:localhost:8080/hello,地址欄自動跳轉到http://localhost:8080/login,進入默認登錄頁面,驗證登陸spring

Username默認爲user,Password隨機生成(實際就是UUID),查看控制檯。數據庫

Spring Security默認進行URL訪問進行攔截,並提供了驗證的登陸頁面json

輸入密碼,我這裏目前是c1068cdb-18f3-48f4-b838-7698218d14c4。登陸成功後端

這裏的用戶名和密能夠修改,直接在配置文件中修改登陸名和密碼,如數組

(application.properties)

spring.security.user.name=admin
spring.security.user.password=123
複製代碼

切入源碼

1> 用戶參數

參照源碼,查看靜態內部類。能夠看出,默認用戶的密碼實際就是一個UUID。

(SpringSecurity -- SecurityProperties.java)

@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
    ...
    // 默認用戶
    private User user = new User();
    ...
	
    public static class User {
       // 默認用戶名
        private String name = "user";

        // 默認用戶名的默認密碼,隨機生成
        private String password = UUID.randomUUID().toString();

        // 默認用戶名的角色
        private List<String> roles = new ArrayList<>();

        // 是否生成密碼
        private boolean passwordGenerated = true;

       ...     
    }    
}
複製代碼

2> 用戶名密碼驗證

  • 導入security依賴後,默認訪問的路徑將通過該過濾器,並訪問其無參構造,建立一個新的post方式的登陸請求,路徑爲/login

  • 進入默認登陸頁

  • 經過HttpServletRequest對象獲取到登陸表單中的用戶名和密碼

  • 建立一個用戶名和密碼的令牌對象

  • 處理登錄表單的信息

(SpringSecurity -- UsernamePasswordAuthenticationFilter.java)

// @since spring security 3.0
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

   public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
   public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

   private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
   private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
   private boolean postOnly = true;

    // 構造器,以不區分大小寫的方式post方式和HTTP方法建立匹配器。
   public UsernamePasswordAuthenticationFilter() {
      super(new AntPathRequestMatcher("/login", "POST"));
   }

   public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
      if (postOnly && !request.getMethod().equals("POST")) {
         throw new AuthenticationServiceException(
               "Authentication method not supported: " + request.getMethod());
      }
		
      // 從請求路徑獲取用戶名和密碼 
      String username = obtainUsername(request);
      String password = obtainPassword(request);

       // 空值判斷
      if (username == null) {
         username = "";
      }

      if (password == null) {
         password = "";
      }

       // 去除用戶名首尾空格
      username = username.trim();

       // 生成一個用戶名密碼身份驗證的令牌
      UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
            username, password);

      // 設置身份認證請求的信息
      setDetails(request, authRequest);

       // 返回一個徹底通過身份驗證的對象,包括憑據
      return this.getAuthenticationManager().authenticate(authRequest);
   }
    ....
    
    protected String obtainUsername(HttpServletRequest request) {
		return request.getParameter(usernameParameter);
	}
}    
複製代碼

自定義登陸接口

(爲便於解釋,不引入數據庫信息驗證)

1 實現接口

實現UserDetailsService接口,重寫方法。

(service.MyUserDetailsSerice)

/** * 自定義登陸接口(核心接口,加載用戶特定的數據。) */
@Component
public class MyUserDetailsSerice implements UserDetailsService {
    // 日誌 返回與做爲參數傳遞的類對應的日誌程序
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);


    /** * 校驗,根據用戶名定位用戶 * @param username 標識須要其數據的用戶的用戶名。 * @return 核心用戶信息,一個徹底填充的用戶記錄 * @throws UsernameNotFoundException */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("登陸,用戶名:{}", username);
        return new User(username, "123", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}
複製代碼

2 配置登陸攔截

繼承WebSecurityConfigurerAdapter配置類,重寫裏面的配置方法

配置方法可查看官網springboot或查看EnableWebSecurity接口的註釋信息

(config.MySecurityConfig)

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
// super.configure(http);
        // 基礎配置
        http.httpBasic()
                .and()
                // 身份認證
                .authorizeRequests()
                // 全部請求
                .anyRequest()
                // 身份認證
                .authenticated();
    }        
複製代碼

返回的User實現了UserDetail接口,詳情見切入源碼

3 測試

啓動項目,清除瀏覽器緩存,訪問hello,跳轉到默認登陸頁面,校驗密碼。登陸時,用戶名任意,密碼必須爲123(MyUserDetailsSerice中已配置)。

登陸失敗,控制檯打印,沒有針對id「null」PasswordEncoder(映射的密碼編碼器)

4 加入密碼編碼器組件

繼承PassawordEncoder接口

/** * 用於編碼密碼的服務接口的實現類。 */
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    /** * 編碼原始密碼。一般,良好的編碼算法應用SHA-1或更大的哈希與8字節或更大的隨機生成的鹽相結合。 * @param rawPassword 密碼,一個可讀的字符值序列 * @return */
    @Override
    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    /** * 驗證從存儲中得到的編碼密碼是否與提交的原始密碼匹配。若是密碼匹配,返回true;若是不匹配,返回false。存儲的密碼自己永遠不會被解碼。 * @param rawPassword 預設的驗證密碼。要編碼和匹配的原始密碼 * @param encodedPassword 表單輸入的密碼。來自存儲的編碼密碼與之比較 * @return */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(rawPassword.toString());
    }
}
複製代碼

4 測試

重啓項目,清除瀏覽器緩存,訪問hello。

切入源碼

1 關於WebSecurityConfigurerAdapter可參考接口EnableWebSecurity

(SpringSecurity -- EnableWebSecurity)

/** * Add this annotation to an {@code @Configuration} class to have the Spring Security * ............. * &#064;Override * protected void configure(HttpSecurity http) throws Exception { * http.authorizeRequests().antMatchers(&quot;/public/**&quot;).permitAll().anyRequest() * .hasRole(&quot;USER&quot;).and() * // 更多配置 ... * .formLogin() // 確保基礎表單登陸 * // 爲全部與表單登陸相關聯的URL設置許可證 * .permitAll(); * } * * ................... * @since 3.2 */
...
@Import({ WebSecurityConfiguration.class,
		SpringWebMvcImportSelector.class,
		OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

	// 默認關閉debug模式
	boolean debug() default false;
}

複製代碼

2 security中封裝的默認用戶User的信息 (SpringSecurity -- User.java)

// 
public class User implements UserDetails, CredentialsContainer{
   ...
    private String password;
	private final String username;
    // 用戶權限集合
	private final Set<GrantedAuthority> authorities;
    // 帳戶未過時
	private final boolean accountNonExpired;
    // 帳戶未鎖定
	private final boolean accountNonLocked;
    // 憑據未過時
	private final boolean credentialsNonExpired;
    // 用戶可用
	private final boolean enabled;
    ...
}
複製代碼

密碼加密

1 注入密碼編碼器對象

繼承WebSecurityConfigurerAdapter配置類

在MySecurity中直接注入一個BCryptPasswordEncoder對象。它實現了PasswordEncoder接口,並重寫了encodematches方法

(config.MySecurityConfig.java)

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    /** * 實現使用BCrypt強哈希函數的密碼編碼器。客戶機能夠選擇性地提供「強度」(即BCrypt中的日誌輪數)和SecureRandom 實例。 * 強度參數越大,須要作的工做就越多(指數級)來散列密碼。默認值是10。 * @return */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    ...
}
複製代碼

2 完善服務層

完善MyUserDetailsSerice

(service.MyUserDetailsSerice.java)

@Component
public class MyUserDetailsSerice implements UserDetailsService {
	...
        
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        String password = passwordEncoder.encode("123");
        logger.info("登陸,用戶名:{},密碼:{}", username,password);
        return new User(username, password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}
複製代碼

3 測試

註釋掉MyPasswordEncoder的@component註解,使其失去容器組件身份

使用debug模式,啓動項目,訪問hello。

debug可看到密碼的轉化,原始密碼123加密爲爲$2a10YGYb9i0ZjnTHPlOk/NQb/efrPNOaJq8hJYtdXf8VcdQUi8T8S3Iim

控制檯打印日誌

切入源碼

能夠看到,這裏自動注入的實際上是BCryptPasswordEncoder對象,並調用了encode方法

(SpringSecurity -- BCryptPasswordEncoder)

// 構造器
public BCryptPasswordEncoder() {
	this(-1);
}
public BCryptPasswordEncoder(int strength) {
    ...
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
    ...
}
...
    
public String encode(CharSequence rawPassword) {
    // 鹽值
   String salt;
    // 判斷構造器是否有相應參數
   if (strength > 0) {
      if (random != null) {
          // 經過random和strength生成的salt
         salt = BCrypt.gensalt(strength, random);
      }
      else {
           // 經過strength生成的salt
         salt = BCrypt.gensalt(strength);
      }
   }
    // 無參構造
   else {
       // 調用gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);隨機生成salt
       // GENSALT_DEFAULT_LOG2_ROUNDS = 10
      salt = BCrypt.gensalt();
   }
    // 使用OpenBSD bcrypt方案散列密碼,參數分別爲原始密碼和鹽值
   return BCrypt.hashpw(rawPassword.toString(), salt);
}
複製代碼
  • 這裏BCryptPasswordEncoder使用的無參,使用默認的鹽值,循環10次,生成了散列的密碼。

  • 這裏雖然是123,但每次加密後都不相同,Spring Security在進行密碼加密的時候,生成了一份隨機salt,最終加密的密碼=密碼+隨機salt。

  • 注意這裏的AuthorityUtils的方法,參數包含角色信息。實際業務中,通常以「ROLE_**」規定用戶的角色字段,並在登陸後授予相應權限

/** *從逗號分隔的字符串表示建立一個GrantedAuthority對象數組(例如「ROLE_A,ROLE_B,ROLE_C」) *@param authorityString 逗號分隔的字符串 *@return 經過標記字符串建立的權限 / AuthorityUtils.commaSeparatedStringToAuthorityList("admin") 複製代碼

自定義登陸請求

不使用springsecurity提供的默認登錄界面

1 自定義前端登陸頁

(template.login.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
</head>
<body>
<h2>歡迎登陸</h2>
<form action="/auth/login" method="post">
    <input name="username" type="text" placeholder="請輸入用戶名.."><br/>
    <input name="password" type="password" placeholder="請輸入密碼.."><br/>
    <input type="submit" value="登陸">
</form>
</body>
</html>
複製代碼

2 自定義首頁

(template.index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MAIN首頁</title>
</head>
<body>
<h1>歡迎來到首頁</h1>
</body>
</html>
複製代碼

3 在控制器類中添加跳轉路徑

@Controller
public class UserController {

    // 登陸測試
	...

    // 登陸頁,跳轉到/templates/login.html頁面
    @GetMapping("/login")
    public String login() {
        return "login";
    }

    // 首頁,跳轉到/templates/index.html頁面
    @GetMapping("/index")
    public String index() {
        return "index";
    }
}    
複製代碼

4 修改攔截配置

修改MySecurityConfig中configure方法

(config.MySecurityConfig.java)

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
	...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                // 表單認證
                .formLogin()
                // 登陸頁
                .loginPage("/login")
                // 登陸表單提交地址
                .loginProcessingUrl("/auth/login")
                .and()
                // 身份認證請求
                .authorizeRequests()
                // URL路徑匹配
                .antMatchers("/login").permitAll()
                // 任意請求
                .anyRequest()
                // 身份認證
                .authenticated();

    }
}
複製代碼

loginProcessingUrl("/auth/login")中定義了表單提交地址,但在控制器UserController中並無對應的請求路徑,SpringSecutity默認攔截全部請求,並將URL 302重定向到/login默認登陸頁,使用默認的用戶名密碼便可登陸。

自定義登陸請求狀態

方式一:繼承接口實現

1 自定義登陸成功類

(handler.MyAuthenticationSuccessHandler.java)

/** * 繼承接口,用於處理成功的用戶身份驗證的策略 */
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);

    // 提供了讀取和寫入JSON的功能,能夠與基本pojo類進行交互,也能夠與通用JSON樹模型進行交互,還提供了執行轉換的相關功能。
    @Autowired
    private ObjectMapper objectMapper;

    // 當用戶已成功經過身份驗證時調用。
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        logger.info("登陸成功");
        response.setContentType("application/json;charset=utf-8");
        // writeValueAsString:將java對象序列化爲字符串
        response.getWriter().write(objectMapper.writeValueAsString(authentication));
    }
}

複製代碼

2 自定義登陸失敗類

(handler.MyAuthenticationFailureHandler.java)

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private static final Logger logger = LoggerFactory.getLogger(UserDetailsService.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        logger.info("登陸失敗");
        // http狀態,200,成功
        response.setStatus(HttpStatus.OK.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(objectMapper.writeValueAsString(exception));
    }
}
複製代碼

方式二:修改MySecurityConfig的配置方法

1 添加登陸成功和失敗的處理方法

(config.MySecurityConfig.java)

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable()        
        .formLogin()
        .loginPage("/login")
        .loginProcessingUrl("/auth/login")
        // 登錄成功處理器
        .successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter writer = response.getWriter();
                ObjectMapper om = new ObjectMapper();
                String successMsg = om.writeValueAsString(om.writeValueAsString(authentication));
                writer.write(successMsg);
                writer.flush();
                writer.close();
            }
        })
        // 登錄失敗處理器
        .failureHandler(new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException {
                resp.setContentType("application/json;charset=utf-8");
                PrintWriter writer = resp.getWriter();
                writer.write(new ObjectMapper().writeValueAsString(e));
                writer.flush();
                writer.close();
            }
        })
        .and()
        .authorizeRequests()
        .antMatchers("/login").permitAll()
        .anyRequest()        
        .authenticated();

}
複製代碼

獲取當前用戶信息

(controller.UserController.java)

@Controller
public class UserController {
	...

    // 當前用戶信息
    @GetMapping("/info")
    @ResponseBody
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }
}
複製代碼

測試

啓動項目,訪問/info,登陸成功,檢查F12

相關文章
相關標籤/搜索