JWT ログイン認証

📢 この記事は gemini-2.5-flash によって翻訳されました

セッション技術

セッション:ユーザーがブラウザを開いてWebサーバーのリソースにアクセスすると、セッションが確立され、どちらかが接続を切るまで継続するよ。1つのセッションには、複数のリクエストとレスポンスが含まれることがあるんだ。

セッショントラッキング:ブラウザの状態を維持する方法だよ。サーバーは、複数のリクエストが同じブラウザから来ているかどうかを識別して、同じセッション内の複数のリクエスト間でデータを共有する必要があるんだ。

セッショントラッキングの仕組み:

  • クライアント側セッショントラッキング技術:Cookie
  • サーバー側セッショントラッキング技術:Session
  • トークン技術

CookieはHTTPプロトコルがサポートしている技術で、ブラウザが初めてアクセスした時に、サーバー側がリクエストヘッダーにSet-Cookie: your_cookieを設定するんだ。ブラウザは自動的にCookieをローカルに保存して、次回のアクセス時には自動でリクエストヘッダーにCookie: your_cookieを追加してくれるよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class SessionController {
    // 设置Cookie
    @GetMapping("/c1")
    public Result cookie1(HttpServletResponse response){
        response.addCookie(new Cookie("login_name", "yexca"));
        return Result.success();
    }

    // 获取Cookie
    @GetMapping("/c2")
    public Result cookie2(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();
        for (Cookie c : cookies){
            if(c.getName().equals("login_name")){
                System.out.println("login_name:"+c.getValue());
            }
        }
        return Result.success();
    }
}

でも、モバイル端末だとCookieは使えないし、Cookieはクロスドメインもできないんだ。

同じドメイン:同じプロトコル、IP/ドメイン、ポート

Session

Cookieをベースに実装されていて、ブラウザが初めてリクエストした時にSessionが生成されるんだ。そして、レスポンスヘッダーでSessionのID Set-Cookie: JSESSIONID=session_idが返されて、ブラウザは次回のリクエスト時に自動でそのIDを付けてくれるよ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
public class SessionController {
    // 往HttpSession中存储值
    @GetMapping("/s1")
    public Result session1(HttpSession session){
        log.info("HttpSession_set:{}", session.hashCode());
        session.setAttribute("login_user","yexca");
        return Result.success();
    }

    // 从HttpSession中获取值
    @GetMapping("/s2")
    public Result session2(HttpServletRequest request){
        HttpSession session = request.getSession();
        log.info("HttpSession_get:{}", session.hashCode());

        Object loginUser = session.getAttribute("login_user");
        log.info("loginUser:{}", loginUser);
        return Result.success();
    }
}

Sessionの問題は、もしサーバー側がロードバランシングを使っていて、つまり複数のサーバーがある場合、最初のレスポンスがサーバー1からで、2回目のレスポンスがサーバー2からだと、サーバー2にはSessionがないから使えないってことだね。

JWT トークン

JWTはJSON Web Tokenの略で、公式サイトは https://jwt.io/ だよ。通信する両者間でJSONデータ形式で安全に情報を転送するための、シンプルで自己完結型のフォーマットを定義しているんだ。デジタル署名があるから、これらの情報は信頼できるんだよ。

構成:

  • 第一部:Header(ヘッダー)。トークンタイプ、署名アルゴリズムなんかを記録してるよ。
  • 第二部:Payload(ペイロード)。カスタム情報やデフォルト情報なんかを記録してるんだ。
  • 第三部:Signature(署名)。トークンの改ざんを防いで、安全性を確保するものだよ。Header、Payload、指定された秘密鍵を使って、指定された署名アルゴリズムで計算されるんだ。

依存関係の追加

1
2
3
4
5
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成と解析

 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
class WebApplicationTests {
    // JWT生成
    @Test
    public void jwtGenTest(){

        Map<String,Object> claims = new HashMap<>();
        claims.put("id", "233");
        claims.put("user", "yexca");
        String jwt = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, "yexca") // 签名算法
                .setClaims(claims) //自定义内容(载荷)
                .setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000)) // 有效期为1h
                .compact();
        System.out.println(jwt);
    }
    
// 出力
// eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU
    
    // JWT解析
    @Test
    public void jwtParseTest(){
        Claims claims = Jwts.parser()
                .setSigningKey("yexca") // 密钥
                .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjIzMyIsImV4cCI6MTcwMjc5NzA4OCwidXNlciI6InlleGNhIn0.BqZDxEddGN4g4GyyfvkOtKYv7DVlIF6cWY9PGW2RbUU")
                .getBody(); // 获得第二部分
        System.out.println(claims);
    }
    
// 出力
// {id=233, exp=1702797088, user=yexca}
}

ログイン認証

フィルター (Filter)

Filterは初期のJavaWeb三大コンポーネント(Servlet, Filter, Listener)の一つだよ。フィルターはリソースへのリクエストを途中で受け止めて、ログイン認証とか、統一エンコード処理、センシティブな文字の処理みたいな特殊な機能を実現できるんだ。

クイックスタート

Filterクラスの作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter 初期化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter 傍受");

        // 放行
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter 破棄");
    }
}

Applicationにアノテーションを追加

1
2
@ServletComponentScan // 开启了对servlet组件的支持
@SpringBootApplication

傍受パス

必要に応じて調整できるよ。以下は例ね。

傍受タイプurlPatterns意味
特定パスの傍受/login/loginへのアクセスのみ傍受される
ディレクトリ傍受/emps/*/emps配下のすべてのリソースが傍受され、/emps自体も傍受される
全て傍受/*すべてのリソースへのアクセスが傍受される

単一フィルターの実行ロジック

ブラウザがリクエストを送信 -> リクエストが傍受される -> 許可前ロジックを実行 -> 許可 -> 許可後ロジックを実行 -> ブラウザがレスポンスを受信

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@WebFilter(urlPatterns = "/*") // 拦截路径
public class DemoFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
//        Filter.super.init(filterConfig);
        System.out.println("Filter 初期化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter 傍受、許可前ロジック");

        // 放行
        filterChain.doFilter(servletRequest, servletResponse);

        System.out.println("Filter 傍受、許可後ロジック");
    }

    @Override
    public void destroy() {
//        Filter.super.destroy();
        System.out.println("Filter 破棄");
    }
}

フィルターチェーン

1つのWebアプリケーションで、複数のフィルターを設定できるんだけど、この複数のフィルターが集まってフィルターチェーンを形成するんだ。

順序:アノテーションで設定されたFilterの優先順位は、フィルタークラス名の自然順序で決まるよ。

ロジック:ブラウザがリクエストを送信 -> リクエストがAに傍受される -> Aの許可前ロジックを実行 -> Aが許可 -> リクエストがBに傍受される -> Bの許可前ロジックを実行 -> Bが許可 -> Bの許可後ロジックを実行 -> Aの許可後ロジックを実行 -> ブラウザがレスポンスを受信

ログイン認証

 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
@Slf4j
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 获取请求url
        String requestURI = request.getRequestURI();
        log.info("リクエストURL:{}",requestURI);

        // 判断是否有login
        if(requestURI.contains("login")){
            log.info("ログイン操作なので許可");
            filterChain.doFilter(servletRequest, servletResponse);
            return; // 因为登录操作不需要以下逻辑
        }

        // 非登录,获取令牌
        String jwt = request.getHeader("token");

        // 判断令牌是否有效
        if(!StringUtils.hasLength(jwt)){
            log.info("リクエストヘッダーのトークンが空");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // 校验令牌
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("トークンの解析に失敗");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return;
        }

        // 放行
        log.info("トークンが有効なので許可");
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

インターセプター (Interceptor)

クイックスタート

インターセプターの作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // 目标方法运行前运行,返回ture放行,false不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");
        return true;
    }

    @Override // 目标方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // 视图渲染完成后运行,最后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}

設定の作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Configuration // 配置类
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginCheckInterceptor loginCheckInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
//        WebMvcConfigurer.super.addInterceptors(registry);
        registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**").excludePathPatterns("/login");

    }

}

傍受パス

パス意味
/*1階層パス/emps, /login は /emps/1 にはマッチしない
/**任意の階層パス/emps, /emps/1, /emps/1/2
/emps/*/emps配下の1階層パス/emps/1 は /emps, /emps/1/2 にはマッチしない
/emps/**/emps配下の任意の階層パス/emps, /emps/1, /emps/1/2

傍受フロー

もしフィルターとインターセプターが同時に存在する場合

ブラウザアクセス -> filter 許可前ロジック -> filter 許可 -> DispatcherServlet -> Interceptor preHandle -> Controller -> postHandle -> afterCompletion -> DispatcherServlet -> filter 許可後ロジック -> ブラウザにレスポンス

Filterはすべてのリクエストを傍受するけど、InterceptorはSpring環境内のリソースだけを傍受するよ。

ログイン認証

 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
53
54
55
56
57
58
59
60
61
62
@Slf4j
@Component
public class LoginCheckInterceptor implements HandlerInterceptor {
    @Override // 目标方法运行前运行,返回ture放行,false不放行
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        return HandlerInterceptor.super.preHandle(request, response, handler);
        System.out.println("preHandle");

        // 获取请求url
        String requestURI = request.getRequestURI();
        log.info("リクエストURL:{}",requestURI);

        // 判断是否有login
        if(requestURI.contains("login")){
            log.info("ログイン操作なので許可");
            return true;
        }

        // 非登录,获取令牌
        String jwt = request.getHeader("token");

        // 判断令牌是否有效
        if(!StringUtils.hasLength(jwt)){
            log.info("リクエストヘッダーのトークンが空");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // 校验令牌
        try {
            JwtUtils.parseJWT(jwt);
        }catch (Exception e){
            e.printStackTrace();
            log.info("トークンの解析に失敗");
            // 将对象转换为JSON
            Result error = Result.error("NOT_LOGIN");
            String notLogin = JSONObject.toJSONString(error);
            response.getWriter().write(notLogin);
            return false;
        }

        // 放行
        log.info("トークンが有効なので許可");
        return true;
    }

    @Override // 目标方法运行后运行
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
        System.out.println("postHandle");
    }


    @Override // 视图渲染完成后运行,最后运行
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
        System.out.println("afterCompletion");
    }
}

Visits Since 2025-02-28

Hugo で構築されています。 | テーマ StackJimmy によって設計されています。