Spring Boot + Spring Security自定義用戶認證

  • 引入依賴:
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
		 <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
  • 自定義認證過程 自定義認證的過程須要實現Spring Security提供的UserDetailService接口 ,源碼以下:
public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

loadUserByUsername方法返回一個UserDetail對象,該對象也是一個接口,包含一些用於描述用戶信息的方法,源碼以下:css

public interface UserDetails extends Serializable {
	// 獲取用戶包含的權限,返回權限集合,權限是一個繼承了GrantedAuthority的對象;
    Collection<? extends GrantedAuthority> getAuthorities();
	// 獲取密碼
    String getPassword();
  // 獲取帳號/用戶名
    String getUsername();
	// 帳戶是否過時
    boolean isAccountNonExpired();
	//帳戶是否被鎖定
    boolean isAccountNonLocked();
	//用戶憑證是否過時
    boolean isCredentialsNonExpired();
	//用戶是否可用
    boolean isEnabled();
}
  • 建立實現自定義認證接口的類:
@Configuration
public class UserDetailService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 模擬一個用戶,實際項目中應爲: 根據用戶名查找數據庫,若是沒有記錄則會返回null,有則返回UserDetails對象
        MyUser user = new MyUser();
        user.setUserName(username);
        user.setPassword(this.passwordEncoder.encode("123456"));
        // 輸出加密後的密碼
        System.out.println(user.getPassword());
		// 返回對象以後 會在內部進行認證(密碼/鹽/加密過密碼等)
        return new User(username, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
    }
}
  • 建立用戶類
@Data
public class MyUser implements Serializable {
    private static final long serialVersionUID = 3497935890426858541L;

    private String userName;

    private String password;

    private boolean accountNonExpired = true;

    private boolean accountNonLocked= true;

    private boolean credentialsNonExpired= true;

    private boolean enabled= true;

}
  • 配置類:
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    ...
}
注:PasswordEncoder是一個密碼加密接口,而BCryptPasswordEncoder是Spring Security提供的一個實現方法,咱們也能夠本身實現PasswordEncoder。
    不過Spring Security實現的BCryptPasswordEncoder已經足夠強大,它對相同的密碼進行加密後能夠生成不一樣的結果

啓動項目:訪問http://localhost:8080/login, 即可以使用任意用戶名以及123456做爲密碼登陸系統html

BCryptPasswordEncoder對相同的密碼生成的結果每次都是不同的git

  • 替換默認登陸頁 直接在src/main/resources/resources目錄下定義一個login.html(不須要Controller跳轉)

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登陸</title>
    <link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
    <form class="login-page" action="/login" method="post">
        <div class="form">
            <h3>帳戶登陸</h3>
            <input type="text" placeholder="用戶名" name="username" required="required" />
            <input type="password" placeholder="密碼" name="password" required="required" />
            <button type="submit">登陸</button>
        </div>
    </form>
</body>
</html>

在MySecurityConfig中添加:github

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // 表單登陸
            // http.httpBasic() // HTTP Basic
            .loginPage("/login.html") //指定了跳轉到登陸頁面的請求URL
            .loginProcessingUrl("/login") //對應登陸頁面form表單的action="/login"
            .and()
            .authorizeRequests() // 受權配置
			//.antMatchers("/login.html").permitAll()表示跳轉到登陸頁面的請求不被攔截,不然會進入無限循環
            .antMatchers("/login.html").permitAll()
            .anyRequest()  // 全部請求
            .authenticated()// 都須要認證
			.and().csrf().disable(); // 關閉csrf防護
}

訪問http://localhost:8080/hello ,會看到頁面已經被重定向到了http://localhost:8080/login.html 使用任意用戶名+密碼123456登陸web

在未登陸的狀況下,當用戶訪問html資源的時候,若是已經登錄則返回JSON數據,不然直接跳轉到登陸頁,狀態碼爲401。spring

要實現這個功能咱們將loginPage的URL改成/authentication/require,而且在antMatchers方法中加入該URL,讓其免攔截:數據庫

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // 表單登陸
            // http.httpBasic() // HTTP Basic
            .loginPage("/authentication/require") // 登陸跳轉 URL
            .loginProcessingUrl("/login") // 處理表單登陸 URL
            .and()
            .authorizeRequests() // 受權配置
            .antMatchers("/authentication/require", "/login.html").permitAll() // 登陸跳轉 URL 無需認證
            .anyRequest()  // 全部請求
            .authenticated() // 都須要認證
            .and().csrf().disable();
}

建立控制器MySecurityController,處理這個請求:json

@RestController
public class MySecurityController {
	//RequestCache requestCache是Spring Security提供的用於緩存請求的對象
    private RequestCache requestCache = new HttpSessionRequestCache();
	//DefaultRedirectStrategy是Spring Security提供的重定向策略 
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @GetMapping("/authentication/require")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)   //HttpStatus.UNAUTHORIZED 未認證 狀態碼401
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
				//getRequest方法能夠獲取到本次請求的HTTP信息
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html"))
							//sendRedirect爲Spring Security提供的用於處理重定向的方法
                redirectStrategy.sendRedirect(request, response, "/login.html");
        }
        return "訪問的資源須要身份認證!";
    }
}

上面代碼獲取了引起跳轉的請求,根據請求是否以.html爲結尾來對應不一樣的處理方法。若是是以.html結尾,那麼重定向到登陸頁面,不然返回」訪問的資源須要身份認證!」信息,而且HTTP狀態碼爲401(HttpStatus.UNAUTHORIZED)。緩存

這樣當咱們訪問http://localhost:8080/hello 的時候頁面便會跳轉到http://localhost:8080/authentication/require,springboot

當咱們訪問http://localhost:8080/hello.html 的時候,頁面將會跳轉到登陸頁面。

  • 處理成功和失敗 Spring Security有一套默認的處理登陸成功和失敗的方法:當用戶登陸成功時,頁面會跳轉會引起登陸的請求,好比在未登陸的狀況下訪問http://localhost:8080/hello, 頁面會跳轉到登陸頁,登陸成功後再跳轉回來;登陸失敗時則是跳轉到Spring Security默認的錯誤提示頁面。下面 經過一些自定義配置來替換這套默認的處理機制。

自定義登陸成功邏輯 要改變默認的處理成功邏輯很簡單,只須要實現org.springframework.security.web.authentication.AuthenticationSuccessHandler接口的onAuthenticationSuccess方法便可:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    @Autowired
    private ObjectMapper mapper;
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
		// 將認證信息轉換成jsonString寫入response
        response.getWriter().write(mapper.writeValueAsString(authentication));
    }
}

其中Authentication參數既包含了認證請求的一些信息,好比IP,請求的SessionId等,也包含了用戶信息,即前面提到的User對象。經過上面這個配置,用戶登陸成功後頁面將打印出Authentication對象的信息。

要使這個配置生效,咱們還在MySecurityConfig的configure中配置它:

@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationSucessHandler authenticationSucessHandler;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表單登陸
                // http.httpBasic() // HTTP Basic
                .loginPage("/authentication/require") // 登陸跳轉 URL
                .loginProcessingUrl("/login") // 處理表單登陸 URL
                .successHandler(authenticationSucessHandler) // 處理登陸成功
                .and()
                .authorizeRequests() // 受權配置
                .antMatchers("/authentication/require", "/login.html").permitAll() // 登陸跳轉 URL 無需認證
                .anyRequest()  // 全部請求
                .authenticated() // 都須要認證
                .and().csrf().disable();
    }
}

咱們將MyAuthenticationSucessHandler注入進來,並經過successHandler方法進行配置。

這時候重啓項目登陸後頁面將會輸出以下JSON信息:

像password,credentials這些敏感信息,Spring Security已經將其屏蔽。

除此以外,咱們也能夠在登陸成功後作頁面的跳轉,修改MyAuthenticationSucessHandler:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
    }
}

經過上面配置,登陸成功後頁面將跳轉回引起跳轉的頁面。若是想指定跳轉的頁面,好比跳轉到/index,能夠將savedRequest.getRedirectUrl()修改成/index:

@Component
public class MyAuthenticationSucessHandler implements AuthenticationSuccessHandler {
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        redirectStrategy.sendRedirect(request, response, "/index");
    }
}

在IndexController中定義一個處理該請求的方法:

@RestController
public class IndexController {
    @GetMapping("index")
    public Object index(){
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

登陸成功後,即可以使用SecurityContextHolder.getContext().getAuthentication()獲取到Authentication對象信息。除了經過這種方式獲取Authentication對象信息外,也能夠使用下面這種方式:

@RestController
public class IndexController {
    @GetMapping("index")
    public Object index(Authentication authentication) {
        return authentication;
    }
}

重啓項目,登陸成功後,頁面將跳轉到http://localhost:8080/index:

  • 自定義登陸失敗邏輯 和自定義登陸成功處理邏輯相似,自定義登陸失敗處理邏輯須要實現org.springframework.security.web.authentication.AuthenticationFailureHandler的onAuthenticationFailure方法:
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
    }
}

onAuthenticationFailure方法的AuthenticationException參數是一個抽象類,Spring Security根據登陸失敗的緣由封裝了許多對應的實現類,

不一樣的失敗緣由對應不一樣的異常,好比用戶名或密碼錯誤對應的是BadCredentialsException,用戶不存在對應的是UsernameNotFoundException,用戶被鎖定對應的是LockedException等。

假如咱們須要在登陸失敗的時候返回失敗信息,能夠這樣處理:

@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {

    @Autowired
    private ObjectMapper mapper;

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}

狀態碼定義爲500(HttpStatus.INTERNAL_SERVER_ERROR.value()),即系統內部異常。

一樣的,咱們須要在BrowserSecurityConfig的configure中配置它:

@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationSucessHandler authenticationSucessHandler;

    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin() // 表單登陸
                // http.httpBasic() // HTTP Basic
                .loginPage("/authentication/require") // 登陸跳轉 URL
                .loginProcessingUrl("/login") // 處理表單登陸 URL
                .successHandler(authenticationSucessHandler) // 處理登陸成功
                .failureHandler(authenticationFailureHandler) // 處理登陸失敗
                .and()
                .authorizeRequests() // 受權配置
                .antMatchers("/authentication/require", "/login.html").permitAll() // 登陸跳轉 URL 無需認證
                .anyRequest()  // 全部請求
                .authenticated() // 都須要認證
                .and().csrf().disable();
    }
}

重啓項目以後,使用錯誤的密碼登陸 圖示以下:

本博文代碼均通過測試,能夠正常運行!

源碼地址: https://github.com/ttdys/springboot/tree/master/springboot_security/02_custom_authentication

相關文章
相關標籤/搜索