- 引入依賴:
<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