這周寫了一下後臺登陸,老師叫我參考一下教程後臺,正好經過此次機會學習一下spring Security。html
咱們看一下官網對於spring security的介紹java
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements
這段文字的大體意思是:
Spring Security是一個強大的、可高度定製化的身份驗證和訪問控制的框架,它基本上是保護基於Spring應用的安全標準。
Spring Security是一個專一於向Java應用程序提供身份驗證和受權的框架。像全部的Spring項目同樣,Spring Security的真正威力在於它能夠很容易地被擴展以知足定製需求。web
咱們開發一個後臺,一些資源想要供全部人訪問,一些資源則只想讓登陸的人訪問,這時候就須要用到咱們的spring security。spring security將身份驗證抽離於業務代碼以外。spring
首先在配置文件中引入spring security的依賴typescript
<!-- Spring Security的核心依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
此時,spring security就已經起做用了,咱們再向後臺發送信息就會被攔截。
這是由於Spring Boot項目引入了Spring Security之後,自動裝配了Spring Security的環境,Spring Security的默認配置是要求通過了HTTP Basic認證成功後才能夠訪問到URL對應的資源,且默認的用戶名是user,密碼則是一串UUID字符串,輸出到了控制檯日誌裏
這顯然不是咱們想要的認證規則。可是就想前面介紹的那樣,spring security強大的地方就在與咱們能夠自定義認證規則。跨域
咱們如今來分析一下項目的spring security,你也能夠參考官網給的demo
官網demo
項目的大體思路就用戶第一次登陸後臺會給一個token,再次請求時就帶着token,後臺經過token與用戶信息綁定,從而知道登陸用戶是誰。這裏的token是有時效的,當token過時後,從新發送token給瀏覽器,瀏覽器緩存起來。帶着這個思路讓咱們看一下代碼實現。瀏覽器
@Configuration @EnableWebSecurity public class MvcSecurityConfig extends WebSecurityConfigurerAdapter { public static String xAuthTokenKey = "x-auth-token"; @Override protected void configure(HttpSecurity http) throws Exception { http // 設置受權配置 .authorizeRequests() // 規定開放端口與須要認證端口 .antMatchers("/teacher/login").authenticated() .antMatchers("/teacher/me").authenticated() .antMatchers("/teacher/logout").authenticated() .antMatchers("/teacher/**").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() // 設置cors過濾器 .and().cors() // 設置httpBasic認證 .and().httpBasic() // 禁用csrf過濾器 .and().csrf().disable() // 在 basic 認證過濾器先後加入自定義過濾器 .addFilterBefore(this.headerAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterAfter(this.addAuthHeaderFilter, BasicAuthenticationFilter.class); } }
咱們自定義一個MvcSecurityConfig
繼承WebSecurityConfigurerAdapter
來自定義咱們的認證規則
再覆蓋父類的configure
方法,在此方法裏自定義規則。
首先
咱們須要規定哪些接口能夠做爲公共資源任意訪問,哪些接口只能登陸後才能夠訪問。經過antMatchers(url).authenticated()
規定請求這個url須要認證,
經過antMatchers(url).permitAll()
規定請求這個url不須要認證。
最後將其餘url設置爲須要認證anyRequest().authenticated()
而後增長cors過濾器,CORS (Cross-Origin Resource Sharing,跨域資源共享)CORS介紹
增長httpBasic認證,
而且禁用csrf過濾器,CSRF(Cross Site Request Forgery, 跨站域請求僞造)CSRF介紹
最後,在BasicAuthenticationFilter
過濾器先後加入咱們自定義的過濾器headerAuthenticationFilter
和addAuthHeaderFilter
(經過依賴注入)。緩存
咱們先說headerAuthenticationFilter,headerAuthenticationFilter主要設置token與驗證token是否有效。安全
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 獲取token,且token爲已認證,則設置PreAuthenticatedAuthenticationToken,代表當前用戶已認證 String authToken = request.getHeader(MvcSecurityConfig.xAuthTokenKey); if (authToken == null) { authToken = UUID.randomUUID().toString(); this.userService.bindAuthTokenLoginUsername(authToken, null, false); } else if (this.userService.isAuth(authToken)) { Optional<User> teacherOptional = this.userService.getUserByToken(authToken); if (teacherOptional.isPresent()) { // token有效,則設置登陸信息 PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken( new UserServiceImpl.UserDetail(teacherOptional.get(), new ArrayList<>()), null, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authentication); } } else if (!this.userService.getUserByToken(authToken).isPresent()) { this.userService.bindAuthTokenLoginUsername(authToken, null, false); } response.setHeader(MvcSecurityConfig.xAuthTokenKey, authToken); filterChain.doFilter(new RequestWrapper(request, authToken), response); }
若是用戶第一次登陸,token爲null,生成token並與user爲null綁定,設置其未登陸,而後將token設置在相應頭裏,轉發。
若是用戶非第一次登陸,獲取token並認證token是否有效,有效則設置登陸信息,無效則與user爲null綁定,設置其未登陸。springboot
AddAuthHeaderFilter只有在用戶名密碼正確時纔會觸發,做用是將Basic認證過濾器認證的用戶名與token綁定並設置其已登陸。
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 若是用戶是經過Basic認證過濾器認證的,則將認證的用戶名與xAuthToken相綁定 Authentication authResult = SecurityContextHolder.getContext().getAuthentication(); if (authResult != null && authResult.isAuthenticated() && !(authResult instanceof PreAuthenticatedAuthenticationToken)) { String xAuthToken = request.getHeader(MvcSecurityConfig.xAuthTokenKey); if (xAuthToken == null) { throw new RuntimeException("未接收到xAuthToken,請在前置過濾器中加入有效的xAuthToken"); } TeacherServiceImpl.UserDetail userDetail = (TeacherServiceImpl.UserDetail) authResult.getPrincipal(); this.teacherService.bindAuthTokenLoginUsername(xAuthToken, userDetail.getTeacher(), true); } filterChain.doFilter(request, response); }
那咱們輸入的用戶名密碼在哪裏驗證呢。
首先咱們在執行spring security中的過濾器時是按照順序依次執行的,此被稱爲Spring security過濾器鏈
而咱們上述配置的鏈路大概爲... -> HeaderAuthenticationFilter -> BasicAuthenticationFilter -> AddAuthHeaderFilter ...
通過測試,全部的登陸請求都會觸發HeaderAuthenticationFilter,而只有用戶名密碼密碼正確的登陸請求才會觸發AddAuthHeaderFilter。因此,惟一的解釋就是BasicAuthenticationFilter進行了用戶名密碼驗證。
咱們觀察BasicAuthenticationFilter源碼
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { UsernamePasswordAuthenticationToken authRequest = this.authenticationConverter.convert(request); if (authRequest == null) { this.logger.trace("Did not process authentication request since failed to find username and password in Basic Authorization header"); chain.doFilter(request, response); return; } ... }
裏面調用了authenticationConverter.convert(request)
public UsernamePasswordAuthenticationToken convert(HttpServletRequest request) { String header = request.getHeader("Authorization"); if (header == null) { return null; } else { header = header.trim(); if (!StringUtils.startsWithIgnoreCase(header, "Basic")) { return null; } else if (header.equalsIgnoreCase("Basic")) { throw new BadCredentialsException("Empty basic authentication token"); } else { byte[] base64Token = header.substring(6).getBytes(StandardCharsets.UTF_8); byte[] decoded = this.decode(base64Token); String token = new String(decoded, this.getCredentialsCharset(request)); int delim = token.indexOf(":"); if (delim == -1) { throw new BadCredentialsException("Invalid basic authentication token"); } else { UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(token.substring(0, delim), token.substring(delim + 1)); result.setDetails(this.authenticationDetailsSource.buildDetails(request)); return result; } } } }
看了這個方法就知道前臺在登陸時傳輸用戶名密碼的格式了。
const authString = encodeURIComponent(this.teacher.username) + ':' + encodeURIComponent(this.teacher.password); const authToken = btoa(authString); let httpHeaders = new HttpHeaders(); httpHeaders = httpHeaders.append('Authorization', 'Basic ' + authToken);
token能夠理解爲學生的學生證,咱們經過學生證的方式證實了我是我。具體能夠看
你是誰