目錄
AspectJ
1
2
| graph TD;
AOP ---> SpringAop & AspectJ
|
AOP (概念)
面向切面編程,利用AOP可以對業務邏輯的各個部分進行隔離,使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高開發的效率
不修改原始碼,從而擴充新功能
Filter(過濾器)Interceptor(攔截器)AspectJ(AOP)之差異
1
2
3
4
5
6
7
8
9
10
11
| flowchart LR;
1((使用者))--發送請求
-->Filter\n+統一設置編碼\n+過濾敏感字\n+登入驗證\n+URL級別的訪問權限控制\n+數據壓縮
-->dispatcher
-->Interceptor\n+權限驗證\n+登入驗證\n+性能檢測
-->AOP\n+日誌紀錄
-->2(Controller)
-1[粗糙]--能處理request的精細程度---->-2[細緻]
|
Filter
1
2
3
4
5
6
|
flowchart LR;
1[瀏覽器]--->2{過濾器}--->3[Web資源]
3[Web資源]-->2{過濾器}-->1[瀏覽器]
|
在HttpServletRequest到達Servlet之前,過濾、處理一些資訊,本身依賴Sevlet容器,不能獲取SpringBean的一些資訊,它是javax.servlet.FilterChain
的項目,不是Springboot
可以做什麼
- 修改Request, Response
- 防止xss(Cross-Site-SCripting跨網站指令碼)攻擊
- 包裝二進制流
自定義Filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| /**
* 網路上教學蠻多都是implenments filter,但我建議extend GenericFilterBean
* 會比較方便一點,省去implenments init(), distory()的麻煩
*/
@Slf4j
@Component
@WebFilter(filterName = "f1",urlPatterns = {"*.html","*.jsp","/"}) //filterName就只是一個名稱可以,隨意就好,urlPattern是用來指定哪些url要經過這個過濾器
public class HiFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Hello Hoxton");
chain.doFilter(request,response); //代表這個Filter已經作用完畢,可以把request,response交給下一個Filter了
}
}
|
結果如上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @Slf4j
/**
* 網路上教學蠻多都是implenments filter,但我建議extend GenericFilterBean
* 會比較方便一點,省去implenments init(), distory()的麻煩
*/
public class HiFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("Hello Hoxton");
chain.doFilter(request,response); //代表這個Filter已經作用完畢,可以把request,response交給下一個Filter了
}
}
|
1
2
3
4
5
6
7
8
9
| @Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean heFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean(new HiFilter());
registration.addUrlPatterns("/*"); //配置相關的路徑
return registration;
}
}
|
一些其他的config設置,僅供參考,與上面釋例無關
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| @Configuration
public class FilterConfig {
//test
@Bean
public FilterRegistrationBean<Filter> logProcessTimeFilter() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new LogProcessTimeFilter()); //設定想要使用哪一個Filter
bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
bean.setName("logProcessTimeFilter"); //設置要叫什麼名字
bean.setOrder(0); //設定過濾器的執行順序
return bean;
}
@Bean
public FilterRegistrationBean<Filter> logApiFilter() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new LogApiFilter()); //設定想要使用哪一個Filter
bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
bean.setName("logApiFilter"); //設置要叫什麼名字
bean.setOrder(1); //設定過濾器的執行順序
return bean;
}
@Bean
public FilterRegistrationBean<Filter> printResponseRequestFilter() {
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
bean.setFilter(new PrintResponseRequest()); //設定想要使用哪一個Filter
bean.addUrlPatterns("/*"); //設置哪些url會觸發Filter,設置成/* 就代表全部都會吃到,/user/*就代表/user開頭的都會吃到
bean.setName("printResponseRequestFilter"); //設置要叫什麼名字
bean.setOrder(2); //設定過濾器的執行順序
return bean;
}
}
|
SpringBoot本身也提供了許多不同的Filter供使用,參考如下
常用的有以下幾個
- CharacterEncodingFilter(用於處理編碼問題)
- HiddenHttpMethodFilter(隱藏Http函數)
- HttpPutFormContentFilter(form表單處理)
- RequesrtContextFilter(請求上下文)
其他資訊可以詳閱Spring MVC中各个filter的用法
其中以OncePerRequestFilter最常被使用,這個Filter會去過濾每一個Request請求,且不會重複執行,且這個Filter有一個doFilterInternal()的方法,供我們撰寫Filter邏輯因doFilter()的方法已在OncePerRequestFilter裡面實現了
,可以用來做Jwtoken的登入驗證,程式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| @Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Autowired
private UserDetailsService userDetailsService;
//注入JwtService UserDetailsService,分別用來解析Token與查詢使用者詳情
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
if (authHeader != null) {
String accessToken = authHeader.replace("Bearer ", "");
//從請求標頭中取得Authorization欄位中的值
Map<String, Object> claims = jwtService.parseToken(accessToken);
//擷取出後面的JWT字串,接著解析它
String username = (String) claims.get("username");
//從claims物件中取得username欄位的值
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//並透過userDetailService查詢使用者詳情。這也代表JWT的內容(payload)必須包含username這個欄位
//在filter中查詢使用者的目的,是為了將該次請求所代表的驗證後資料(Authentication)帶進security中的Context。
//Context是一種較抽象的概念,可以想像成該次請求的身分狀態
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
//為了將這個請求的使用者身分告訴伺服器,我們建立UsernamePasswordAuthenticationToken的物件,其中第三個參數放GrantedAuthority的List, 作為API的授權檢查
//第一個參數(principal)傳入使用者詳請(UserDetails)。
// 而第二個參數是credential,通常是密碼,但不傳入也無訪
//經由傳入principal,我們得以在業務邏輯中從Context輕易獲取使用者身分的資料
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
|
配置完後再將這個Filter加入Security的過濾鍊
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| @EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 協助帳號密碼認證的東西
* @return
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//加入Security的過濾鍊
protected void configure(HttpSecurity http) throws Exception {
http.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/users/**").hasAuthority(MemberAuthority.SYSTEM_ADMIN.name())
// .antMatchers(HttpMethod.GET,"/h2/**").hasAuthority(MemberAuthority.SYSTEM_ADMIN.name())
.antMatchers(HttpMethod.GET,"/login/**").permitAll()
// .antMatchers(HttpMethod.POST,"login").permitAll()
// .antMatchers(HttpMethod.POST, "/users").permitAll()
.anyRequest().permitAll()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //於UsernamePasswordAuthenticationFilter進行認證
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.formLogin();
http.headers().frameOptions().disable();
//讓spring Security可以和h2建立連線
}
/**
*
* @param auth 配置全局驗證資訊,如Authentication Provider,UserDetailService等等資訊,
* authenticationManager會接收到UsernamePasswordAuthenticationToken傳入的資料後
* 調用SecurityConfig中所配置的userDetailsService,passwordEncoder來協助驗證
*
* @throws Exception
*/
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
|
一些Code的示範
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public class LogProcessTimeFilter extends OncePerRequestFilter {
/**
* @param request 請求
* @param response 回應
* @param filterChain 過濾鏈 會將現有的filter給串聯起來,當請求進入後端,需要依序經過它們才會達到Controller,相對的,當回應離開Controller,則是按照相反的方向經過那些Filter
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
filterChain.doFilter(request, response); //doFilter:相當於將請求送至Controller。
long endTime = System.currentTimeMillis();
long processTime = endTime - startTime;
System.out.println("processTime = " + processTime);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| /**
* Controller收到的請求主體(RequestBody)和回應主體(ResponseBody)
* 分別由HttpServletRequest與HttpServletResponse的InputStream、OutputStream轉化而來,
* 但資料流只能讀取一次,如果在Filter層就被讀掉,可能會導致後面都收不到資料
* 為了保留主體中的資料,我們將請求主體與回應主體包裝成ContentCachingResponseWrapper ContentCachingRequestWrapper
* 再如同往常傳入FilterChain
*
* 這兩個Wrapper的特色是會在內部備份一個ByteArrayOutputStream,我們只要呼叫這兩個Wrapper的
* getContentAsByteArray就可以無限制地取得主體內容
*/
public class PrintResponseRequest extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
filterChain.doFilter(requestWrapper, responseWrapper);
// logApi(request, response);
logBody(requestWrapper,responseWrapper);
responseWrapper.copyBodyToResponse();
}
private void logApi(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int httpStatus = response.getStatus(); //200,403,404之類的
String httpMethod = request.getMethod();
String uri = request.getRequestURI();
String params = request.getQueryString();
if (params != null) {
uri += "?" + params;
}
System.out.println(String.join(" ", String.valueOf(httpStatus), httpMethod, uri));
}
private void logBody(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
String requestBody = getContent(request.getContentAsByteArray());
System.out.println("Request: " + requestBody);
String responseBody = getContent(response.getContentAsByteArray());
System.out.println("Response: " + responseBody);
}
/**
* @param content
* @return 返回JSON字串
*/
private String getContent(byte [] content){
String body = new String(content);
return body.replaceAll("[\n\t]", ""); //去除換行\n與定位符號\t
}
}
|
Interceptor
本身是AOP的一種應用,其實攔截器跟過濾器是可以互相替換的,功能其實差不多,只是攔截器可以在請求到達Controller或是回應回傳出Contrller時進行攔截,攔截成功時可以實做一些自定義的業務邏輯進行修改,且Interceptor是Springboot下的一個功能org.springframework.web.servlet.HandlerInterceptor
可以用來
- 性能監控:紀錄請求的處理時間,比如說請求處理太久(超過500毫秒)
- 登入檢測
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| graph LR;
request-->id1
id1-->id2-->id4-->id3-->id6-->id5
subgraph 攔截器1
direction TB
id1["preHandle()"]
id3["postHandler()"]
id5["afterCompletion()"]
end
subgraph 攔截器2
direction TB
id2["preHandle()"]
id4["postHandler()"]
id6["afterCompletion()"]
end
|
要實現interceptor有兩種方式
- 實作HandlerInterceptor
- 繼承HandlerInterceptorAdapter
釋例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public class LogInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
long startTime = System.currentTimeMillis();
System.out.println("\n-------- LogInterception.preHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("Start Time: " + System.currentTimeMillis());
request.setAttribute("startTime", startTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("\n-------- LogInterception.postHandle --- ");
System.out.println("Request URL: " + request.getRequestURL());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("\n-------- LogInterception.afterCompletion --- ");
long startTime = (Long) request.getAttribute("startTime");
long endTime = System.currentTimeMillis();
System.out.println("Request URL: " + request.getRequestURL());
System.out.println("End Time: " + endTime);
System.out.println("Time Taken: " + (endTime - startTime));
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| @Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor());
registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");
registry.addInterceptor(new AdminInterceptor()).addPathPatterns("/admin/*").excludePathPatterns("/admin/oldLogin");
}
}
|
AspectJ
屬於一種AOP框架
AOP(JDK動態代理)
使用JDK的動態代理,要使用Proxy類裡面的方法來創建出代理對象 newProxyInstance(類加載器,增強方法所在的類,這個類實現的介面,實現這個接口(InvocationHandler)
編寫JDK動態代碼
1
2
3
4
5
6
| public interface UserDao {
public int add (int a,int b);
public String update(String id);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| public class UserDaoImpl implements UserDao{
@Override
public int add(int a, int b) {
System.out.println("add方法執行了");
return a+b;
}
@Override
public String update(String id) {
return id;
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| package com.example.aop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* @author Hoxton
* @version 1.1.0
*/
public class JDKProxy {
public static void main(String[] args) {
Class[] interfaces = {UserDao.class};
UserDaoImpl userDao = new UserDaoImpl();
UserDao dao = (UserDao) Proxy.newProxyInstance(JDKProxy.class.getClassLoader(), interfaces, new UserDaoProxy(userDao));
//此dao已經不是原本的dao,而是新的代理類dao了
int result = dao.add(1, 2);
System.out.println("result = " + result);
}
}
//創建代理對象的代碼
class UserDaoProxy implements InvocationHandler {
//1. 把創建的是誰的代理對象,把誰傳遞進來
// 有參建構子
private Object obj;
public UserDaoProxy(Object obj) {
this.obj = obj;
}
//增強的邏輯
@Override
public Object invoke(Object proxy, Method method, Object[] methodArgs) throws Throwable {
//方法之前
System.out.println("方法之前執行..." + method.getName() + "傳遞的參數..." + Arrays.toString(methodArgs));
//被增強的方法執行
Object res = method.invoke(obj, methodArgs);
//方法之後
System.out.println("方法之後執行..." + obj);
return res;
}
}
|
AOP專業術語
連接點
一個類裡面中,能被增強的方法就叫連接點,下面這個類就有四個連接點
1
2
3
4
5
6
| class User{
add();
update();
select();
delete();
}
|
切入點
實際被增強的方法,就叫切入點
通知(增強)
實際增強的邏輯部分稱為通知(增強)
通知有多種類型
前置通知
在切入點前執行
後置通知
在切入點後執行
環繞通知
在切入點前後執行
異常通知
出現異常時執行
最終通知
執行到try…catch的final時執行
切面
是一個動作
- 把通知應用到切入點的過程,就叫切面
AOP(準備)
Spring 框架一般都是基於AspectJ實現的AOP操作
什麼是AspectJ
- AspectJ不是Spring的組成部分,是一個獨立的AOP框架, 一般把AspectJ和Spring框架一起使用,進行AOP操作
基於Aspect實現AOP操作
- xml配置文件實現
- 基於註解方法實現(主要使用)
再專案裡面引入AOP依賴
切入點表達式
切入點表達式的作用: 知道對哪個類的哪個方法進行增強
語法結構:
execution( [權限修飾符] [返回類型] [類全路徑] [方法名稱] ( [參數列表] ) )
權限修飾符: public, private, *(代表不論是public, private 都選)
返回類型: String, int
類全路徑: com.hoxton…….
方法名稱: 就方法名稱
參數列表: 有哪些參數
舉例
對com.hoxton.dao.BookDao類裡面的add方法進行增強
1
| execution(* com.hoxton.dao.BookDao.add(..) )
|
對com.hoxton.dao.BookDao類的所有方法進行增強
1
| execution(* com.hoxton.dao.BookDao.*(..))
|
對com.hoxton.dao包裡的所有類,類裡面的髓有方法進行增強
1
| excution(* com.hoxton.dao.*.*(..))
|
within([package名].* )或( [package名]..*)
舉例
AOP操作(Aspect J 註解)
- 創建類,在類裡面定義方法
1
2
3
4
5
6
| public class User {
public void add(){
System.out.println("add");
}
}
|
創建增強類(編寫增強邏輯)
在增強類的裡面,創建方法,讓不同方法代表不同通知類型
1
2
3
4
5
| public class UserProxy {
public void before(){
System.out.println("before");
}
}
|
進行通知的配置
- 在Spring
Log4j 2
1
2
3
4
| flowchart TD;
8["ALL(全輸出不留情)"]--->7["Trace(更細的除錯資訊,通常用來追蹤城市流程的日誌)"]--->6["DEBUG(除錯資訊的日志)推薦★"]--->5["INFO(一般資訊的日志)推薦★"]--->4["WARN(可能導致錯誤的日志)"]--->3["ERROR(造成應用錯誤停止的日志)"]--->2["FETAL(造成應用程式停止的日志)"]--->1["OFF(不輸出任何日志)"]
|
參考
https://www.cnblogs.com/itlihao/p/14329905.html
https://blog.csdn.net/fly910905/article/details/86537648
SpringBoot攔截器(Interceptor)詳解
Spring Boot使用過濾器和攔截器分別實現REST介面簡易安全認證