Spring Security學習總結
關於設計模式的文章在公眾號上面已經更新完了,感興趣的小夥伴可以關注公眾號每天學Java隨時檢視!
學習Spring Security是在一個客戶的專案上看到客戶在自己專案上使用Spring Security,當時很好奇這麼框架是幹嘛的,就私下問了他們的技術人員:Security是做什麼的。他大致的意思是說,Security是一個提供安全許可權控制的框架,利用這個框架可以為系統中的人員定製相關的許可權,實現無權的攔截。
所以我就私下學習一下這個框架,個人覺得還不錯(一開始就使用前後端分離,利用jquery的ajax進行訪問介面).
學習這個框架的時候我覺得最主要是要明白HttpSecurity這個類,他是允許為特定的http請求配置基於web的安全性。預設情況下,它將應用於所有請求,但是可以使用requestMatcher(requestMatcher)或其他類似的方法進行限制。換句話說,就是通過這個HttpSecurity這個類我們對於HTTP請求進行控制,比如說是否攔截。
這個類的所有方法可以去檢視官方文件,我這裡主要說幾個我用到的:
addFilterBefore(javax.servlet.Filter filter, java.lang.Class<? extends javax.servlet.Filter> beforeFilter) | 在已知的過濾器類之前新增過濾器。 |
antMatcher(java.lang.String antPattern) | 只有匹配提供的路徑時呼叫配置的HttpSecurity。 |
authorizeRequests() | 允許使用HttpServletRequest限制訪問,在他的後面我們會通過antMatcher增加一些請求路徑的過濾。或者使用anyRequest() |
cors() | 新增要使用的CorsFilter。也就是允許跨域 |
csrf() | 新增CSRF的支援。 |
formLogin() | 指定支援基於表單的身份驗證。 |
sessionManagement() | 配置會話管理。 |
然後就是WebSecurityConfigurerAdapter這個類,它為建立WebSecurityConfigurer例項提供了一個方便的基類。該實現允許通過覆蓋方法進行定製。看下面的程式碼:我們繼承這個類,重寫他的config方法(三個config方法引數不同),我們主要看引數是HttpSecurity的這個方法,具體的可以看一下註解。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private EntryPoint entryPoint;
@Autowired
private JwtAjaxFilter jwtAjaxFilter;
@Autowired
private MyUserService myUserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//我的專案是使用Ajax訪問介面,所以這裡取消Session。
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
//httpBasic返回HttpBasicConfigurer物件,
//HttpBasicConfigurer物件的authenticationEntryPoint方法是在身份驗證失敗時,將觸發AuthenticationEntryPoint類commence方法
.httpBasic().authenticationEntryPoint(entryPoint)//登陸失敗執行的介面
.and()
//authorizeRequests返回ExpressionUrlAuthorizationConfigurer物件,.anyRequest().access會去進行URL對映使得
.authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//角色資源
.and()
//這個URL比較特殊, Security自帶無需定義
.formLogin().loginPage("/login")
//登入成功觸發
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登入成功");
AjaxResponseBody responseBody = new AjaxResponseBody();
responseBody.setStatus("200");
responseBody.setMsg("Login Success!");
UserVO userVO = (UserVO) authentication.getPrincipal();
//生成Token
String JWT = JwtUtil.generateToken(userVO.getUsername(), 30000);
responseBody.setJwtToken(JWT);
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
})
//登入成功觸發
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println("登入失敗");
AjaxResponseBody responseBody = new AjaxResponseBody();
responseBody.setStatus("400");
responseBody.setMsg("Login Failure!");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
})
.permitAll()
.and()
.cors()
.and()
.csrf().disable();//允許跨域
//403
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
System.out.println("沒有許可權");
AjaxResponseBody responseBody = new AjaxResponseBody();
responseBody.setStatus("403");
responseBody.setMsg("need Authorities!");
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
});
//攔截
http.addFilterBefore(jwtAjaxFilter, UsernamePasswordAuthenticationFilter.class);
// 禁用快取
http.headers().cacheControl();
}
//自定義認證方式
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserService).passwordEncoder(new BCryptPasswordEncoder());
}
}
上面的程式碼剛開始讓我最為難的地方,因為在部落格找的案例完全看不懂,後來沒有辦法,根據HttpSecurity這個引數一點點去向下研究,於是大致明白Spring Boot結合Spring Security的專案中WebSecurityConfigurerAdapter是配置的關鍵,但是HttpSecurity是關鍵中關鍵,通過它我們會牽扯出很多類出來,就像上面的程式碼一樣。下面具體來說明一下自己學習的經過:
首先是通過IDEA新建一個Spring Security應用。
當應用載入完成之後我們就啟動它,我們會發現在控制檯中出現一行密碼
使用瀏覽器訪問127.0.0.1:8080的時候會預設到登入介面,這個登入介面是應用自帶的,我們輸入user和控制檯的密碼就登入成功了。(上面的步驟在公眾號文章中配有圖,可關注檢視)
這個時候應用就算是跑起來了,但是這個時候的登入是使用框架自帶的並非我們自定義登入驗證的。
這是我遇到的第一個問題:如何實現自定義登入驗證
由於Spring Security自帶的有認證方式,所以如果我們使用這個框架,我們肯定要重寫這些認證方式,最終我們得知UserDetails,UserDetailsService類是進行人員角色認證的。所以我們要實現這兩個類:
然後我們再配合 WebSecurityConfigurerAdapter的自定義子類成功的實現了使用者登入(程式碼在最上面),但是後續我就發現一個問題:在我登入成功之後,我訪問資料介面的時候仍然會報錯,因為請求被攔截了,其實很好理解:因為前後端分離,每一次請求都是無狀態的,服務端無法通過Session獲取我的使用者資訊,怎麼辦呢?Token處理。思路如下:
在我們登入請求/login這個介面的時候服務端是不會進行攔截的,所以我們首先利用這個介面登入,但是不同的是,我們在登入成功後要返回一個Token,這個Token儲存的有使用者資訊。如下程式碼,通過JwTUtil生成字串,這裡我是使用網上的程式碼,我們自己可以通過JDK生成jks檔案,然後進行RSA方式加密使用者資訊。(當然我們還要引用JWT的依賴)
//登入成功觸發
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
System.out.println("登入成功");
AjaxResponseBody responseBody = new AjaxResponseBody();
responseBody.setStatus("200");
responseBody.setMsg("Login Success!");
UserVO userVO = (UserVO) authentication.getPrincipal();
//生成Token,有效時常3分鐘
String JWT = JwtUtil.generateToken(userVO.getUsername(), 30000);
responseBody.setJwtToken(JWT);
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS, HEAD");
httpServletResponse.getWriter().write(JSON.toJSONString(responseBody));
}
})
然後Ajax會接收到這個Token。然後我們將Token儲存到sessionStorage中,在下一次我們訪問非login介面時,我們header中要攜帶Token,但是問題又來了,服務端如何拿到這個Token呢?雖然Spring Boot攔截器可以實現,但是Spring Security該怎麼辦?我們在最開始說HttpSecurity的時候又這個方法addFilterBefore,我們可以通過它增加一個攔截器在Security自己的攔截器之前執行,然後在這裡面進行判斷Token是否合法或者是否登入。具體程式碼如下:
@Component
public class JwtAjaxFilter extends OncePerRequestFilter {
@Autowired
private MyUserService myUserService;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String authHeader = httpServletRequest.getHeader("Authorization");
System.out.println(authHeader);
if (authHeader != null && authHeader.startsWith("Bearer ")) {
System.out.println("獲取Bearer");
final String openId = authHeader.substring("Bearer ".length());
String username = JwtUtil.parseToken(openId);
System.out.println("username==="+username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
System.out.println("userName=" + username);
UserDetails userDetails = myUserService.loadUserByUsername(username);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
System.out.println(1);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
到這裡我們基本上就實現了前後端分離的形式來達成安全訪問。但是許可權控制好像沒有體現到。怎麼做呢?
.authorizeRequests().anyRequest().access("@rbacauthorityservice.hasPermission(request,authentication)")//角色資源
在我的配置程式碼中有上面這麼一行程式碼。這裡實現了URL和角色的關聯,更具體的實現就看下面的程式碼
@Component("rbacauthorityservice")
public class RbacAuthorityService {
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
//得到的principal的資訊
Object o = authentication.getPrincipal();
//預設是未授權角色
boolean hasPermission = false;
//獲得的物件是否實現UserDetails
if (o instanceof UserDetails) {
//取得使用者名稱
String userName = ((UserDetails) o).getUsername();
// Authentication是一個介面,用來表示使用者認證資訊的
Collection<? extends GrantedAuthority> authorities = ((UserDetails) o).getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
for (GrantedAuthority authority : authorities) {
//輸出角色
System.out.println(authentication.getAuthorities());
//管理員角色
if (authority.getAuthority().equals("ROLE_ADMIN")) {
//管理員可以訪問的URL
Set<String> urls = new HashSet();
urls.add("/sys/**");
urls.add("/test/**");
//AntPathMatcher用來做類URLs字串匹配
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
}
}
//普通使用者可以訪問的URL
Set<String> urls = new HashSet();
urls.add("/test/**");
urls.add("/pay/**");
AntPathMatcher antPathMatcher = new AntPathMatcher();
for (String url : urls) {
if (antPathMatcher.match(url, request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
}
return hasPermission;
}
}
更多文章請關注公眾號:每天學Java。想獲得更多最新面試提醒請進入小程式:每天學Java
公眾號二維碼: 小程式二維碼: