春。JWTを使用したカスタム認証

この記事では、私の意見では、JWTを使用してRESTAPIでユーザー認証用のバイクを作成した成功した経験を共有したいと思います。

Spring Securityに代わるものではありませんが、2年以上にわたって生産が順調に進んでいます。



JWTのキーの生成からコントローラーまで、プロセス全体を可能な限り詳細に説明して、JWTに慣れていない人でもすべてを理解できるようにします。







コンテンツ



  • バックグラウンド
  • キー生成
  • 春のプロジェクト作成
  • TokenHandler
  • 注釈とハンドラー
  • AuthenticationExceptionの処理
  • コントローラ


0.背景



まず、このクライアント認証方法を実装するきっかけとなった理由と、SpringSecurityを使用しなかった理由を説明します。興味がない場合は、次の章にスキップできます。



その時まで、私はウェブサイトを開発する小さな会社で働いていました。これがこの分野での私の最初の仕事だったので、私は本当に何も知りませんでした。約1か月の作業の後、新しいプロジェクトがあり、そのための基本的な機能を準備する必要があると彼らは言いました。このプロセスが既存のプロジェクトでどのように実装されているかを詳しく見ることにしました。残念ながら、すべてがそれほど幸せではありませんでした。



許可されたユーザーを引き出す必要があるコントローラーの各方法では、次のようなものがありました。



@RequestMapping(value = "/endpoint", method = RequestMethod.GET)
 public Response endpoint() {
     User user = getUser(); //   
     if (null == user)
         return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();

     //  
 }


そして、それはいたるところにありました...新しいエンドポイントの追加は、このコードがコピーされたという事実から始まりました。私はそれが少し奇妙で、使用するのが完全に厄介であることに気づきました。



この問題を解決するために、私はグーグルに行きました。何か間違ったものを探していたのかもしれませんが、適切な解決策を見つけることができませんでした。 SpringSecurityを構成するための手順はいたるところにありました。



SpringSecurityを使いたくない理由を説明しましょう。複雑すぎて、RESTで使用するにはどういうわけかあまり便利ではないように思えました。また、エンドポイントの処理方法では、おそらくユーザーをコンテキストから外す必要があります。私はそれについてあまり知らなかったので、おそらく私は間違っていますが、記事はとにかくそれについてではありません。



シンプルで使いやすいものが必要でした。注釈を介してそれを行うというアイデアが生まれました。



アイデアは、承認が必要なコントローラーの各メソッドにユーザーを注入することです。そしてそれがすべてです。コントローラメソッド内にはすでに許可されたユーザーが存在し、!= Nullになります(許可が不要な場合を除く)。



このバイクを作った理由を見つけました。それでは、練習に取り掛かりましょう。



1.キーの生成



まず、ユーザーに関する最低限必要な情報を暗号化するキーを生成する必要があります。



jwtを使用してjavaで作業するための非常に便利なライブラリがあります。



githubには、jwtの操作方法に関するすべての説明がありますが、プロセスを簡略化するために、以下に例を示します。



キーを生成するには、通常のmavenプロジェクトを作成し、次の依存関係を追加します



依存関係
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>




そして秘密を生み出すクラス



SecretGenerator.java
package jwt;

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class SecretGenerator {

    public static void main(String[] args) {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS512);
        String secretString = Encoders.BASE64.encode(secretKey.getEncoded());
        System.out.println(secretString);
    }
}




その結果、将来使用する秘密鍵を取得します。



2.Springプロジェクトの作成



このトピックに関する記事やチュートリアルがたくさんあるので、作成プロセスについては説明しません。また、Springの公式Webサイトには、2回のクリックで最小限のプロジェクトを作成できるイニシャライザーがあります。



最終的なpomファイルのみを残します



pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
    </parent>

    <groupId>org.website</groupId>
    <artifactId>backend</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>14</java.version>
        <start-class>org.website.BackendWebsiteApplication</start-class>
    </properties>

    <profiles>
        <profile>
            <id>local</id>
            <properties>
                <activatedProperties>local</activatedProperties>
            </properties>
            <activation>
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>
    </profiles>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <dependencies>
        <!--*******SPRING*******-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--*******JWT*******-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.2</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>0.11.2</version>
            <scope>runtime</scope>
        </dependency>

        <!--*******OTHER*******-->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.14</version>
        </dependency>
        <dependency>
            <groupId>org.liquibase</groupId>
            <artifactId>liquibase-core</artifactId>
            <version>4.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>29.0-jre</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
            <scope>provided</scope>
        </dependency>

        <!--*******TEST*******-->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>2.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>




プロジェクトを作成したら、以前に作成したキーをapplication.propertiesにコピーします



app.api.jwtEncodedSecretKey=teTN1EmB5XADI5iV4daGVAQhBlTwLMAE+LlXZp1JPI2PoQOpgVksRqe79EGOc5opg+AmxOOmyk8q1RbfSWcOyg==


3.TokenHandler



トークンを生成および復号化するためのサービスが必要になります。



トークンには、ユーザーに関する最小限の情報(IDのみ)とトークンの有効期限が含まれます。これを行うには、インターフェイスを作成します。



トークンの有効期間を転送します。



Expiration.java
package org.website.jwt;

import java.time.LocalDateTime;
import java.util.Optional;

public interface Expiration {

    Optional<LocalDateTime> getAuthTokenExpire();
}




そしてIDを転送するため。ユーザーエンティティによって実装されます



CreateBy.java
package org.website.jwt;

public interface CreateBy {

    Long getId();
}




また、Expirationインターフェイスのデフォルトの実装を作成しますデフォルトでは、トークンは24時間存続します。



DefaultExpiration.java
package org.website.jwt;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.Optional;

@Component
public class DefaultExpiration implements Expiration {

    @Override
    public Optional<LocalDateTime> getAuthTokenExpire() {
        return Optional.of(LocalDateTime.now().plusHours(24));
    }
}




いくつかのヘルパークラスを追加しましょう。



GeneratedTokenInfo-生成されたトークンに関する情報。

TokenInfo-私たちに届いたトークンに関する情報。



GeneratedTokenInfo.java
package org.website.jwt;

import java.time.LocalDateTime;
import java.util.Optional;

public class GeneratedTokenInfo {

    private final String token;
    private final LocalDateTime expiration;

    public GeneratedTokenInfo(String token, LocalDateTime expiration) {
        this.token = token;
        this.expiration = expiration;
    }

    public String getToken() {
        return token;
    }

    public LocalDateTime getExpiration() {
        return expiration;
    }

    public Optional<String> getSignature() {
        if (null != this.token && this.token.length() >= 3)
            return Optional.of(this.token.split("\\.")[2]);

        return Optional.empty();
    }
}





TokenInfo.java
package org.website.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import lombok.NonNull;

import java.time.LocalDateTime;
import java.time.ZoneId;

public class TokenInfo {

    private final Jws<Claims> claimsJws;

    private final String signature;
    private final Claims body;
    private final Long userId;
    private final LocalDateTime expiration;

    private TokenInfo() {
        throw new UnsupportedOperationException();
    }

    private TokenInfo(@NonNull final Jws<Claims> claimsJws,
                      @NonNull final String signature,
                      @NonNull final Claims body,
                      @NonNull final Long userId,
                      @NonNull final LocalDateTime expiration) {
        this.claimsJws = claimsJws;
        this.signature = signature;
        this.body = body;
        this.userId = userId;
        this.expiration = expiration;
    }

    public static TokenInfo fromClaimsJws(@NonNull final Jws<Claims> claimsJws) {
        final Claims body = claimsJws.getBody();
        return new TokenInfo(
                claimsJws,
                claimsJws.getSignature(),
                body,
                Long.parseLong(body.getId()),
                body.getExpiration().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
    }

    public Jws<Claims> getClaimsJws() {
        return claimsJws;
    }

    public String getSignature() {
        return signature;
    }

    public Claims getBody() {
        return body;
    }

    public Long getUserId() {
        return userId;
    }

    public LocalDateTime getExpiration() {
        return expiration;
    }
}




これで、TokenHandler自体がユーザーの承認時にトークンを生成し、以前に承認されたユーザーが使用したトークンに関する情報を取得します。



TokenHandler.java
package org.website.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.sql.Date;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Base64;
import java.util.Optional;

@Service
@Slf4j
public class TokenHandler {

    @Value("${app.api.jwtEncodedSecretKey}")
    private String jwtEncodedSecretKey;

    private final DefaultExpiration defaultExpiration;

    private SecretKey secretKey;

    @Autowired
    public TokenHandler(final DefaultExpiration defaultExpiration) {
        this.defaultExpiration = defaultExpiration;
    }

    @PostConstruct
    private void postConstruct() {
        byte[] decode = Base64.getDecoder().decode(jwtEncodedSecretKey);
        this.secretKey = new SecretKeySpec(decode, 0, decode.length, "HmacSHA512");
    }

    public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy, Expiration expire) {
        if (null == expire || expire.getAuthTokenExpire().isEmpty())
            expire = this.defaultExpiration;

        try {
            final LocalDateTime expireDateTime = expire.getAuthTokenExpire().get().withNano(0);

            String compact = Jwts.builder()
                    .setId(String.valueOf(createBy.getId()))
                    .setExpiration(Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant()))
                    .signWith(this.secretKey)
                    .compact();

            return Optional.of(new GeneratedTokenInfo(compact, expireDateTime));
        } catch (Exception e) {
            log.error("Error generate new token. CreateByID: {}; Message: {}", createBy.getId(), e.getMessage());
        }
        return Optional.empty();
    }

    public Optional<GeneratedTokenInfo> generateToken(CreateBy createBy) {
        return this.generateToken(createBy, this.defaultExpiration);
    }

    public Optional<TokenInfo> extractTokenInfo(final String token) {
        try {
            Jws<Claims> claimsJws = Jwts.parserBuilder()
                    .setSigningKey(this.secretKey)
                    .build()
                    .parseClaimsJws(token);
            return Optional.ofNullable(claimsJws).map(TokenInfo::fromClaimsJws);
        } catch (Exception e) {
            log.error("Error extract token info. Message: {}", e.getMessage());
        }

        return Optional.empty();
    }

}




これですべてが明確になるはずなので、私はあなたの注意を引きません。



4.注釈とハンドラー



それで、すべての準備作業の後、最も興味深いものに移りましょう。前述のように、許可されたユーザーが必要な場合に、コントローラーメソッドに挿入される注釈が必要です。



次のコードで注釈を作成します



AuthUser.java
package org.website.annotation;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthUser {
    boolean required() default true;
}




承認はオプションである可能性があると以前に述べました。このためだけに、要約で必要なメソッドが必要です特定のメソッドの許可がオプションであり、着信ユーザーが実際に許可されていない場合、nullがメソッドに挿入されます。しかし、私たちはこれに備えるでしょう。



注釈は作成されましたが、ハンドラーが引き続き必要です。ハンドラーは、要求からトークンを取得し、ユーザーベースからトークンを受け取り、コントローラーメソッドに渡します。このような場合、SpringにはHandlerMethodArgumentResolverインターフェイスがあります実装します。上記のインターフェイスを実装



するAuthUserHandlerMethodArgumentResolverクラス作成します。



AuthUserHandlerMethodArgumentResolver.java
package org.website.annotation.handler;

import org.springframework.core.MethodParameter;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.WebUtils;
import org.website.annotation.AuthUser;
import org.website.annotation.exception.AuthenticationException;
import org.website.domain.User;
import org.website.domain.UserJwtSignature;
import org.website.jwt.TokenHandler;
import org.website.service.repository.UserJwtSignatureService;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
import java.util.Optional;

public class AuthUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

    private final String AUTH_COOKIE_NAME;
    private final String AUTH_HEADER_NAME;

    private final TokenHandler tokenHandler;

    private final UserJwtSignatureService userJwtSignatureService;

    public AuthUserHandlerMethodArgumentResolver(final String authTokenCookieName,
                                                 final String authTokenHeaderName,

                                                 final TokenHandler tokenHandler,

                                                 final UserJwtSignatureService userJwtSignatureService) {
        this.AUTH_COOKIE_NAME = authTokenCookieName;
        this.AUTH_HEADER_NAME = authTokenHeaderName;

        this.tokenHandler = tokenHandler;

        this.userJwtSignatureService = userJwtSignatureService;
    }

    @Override
    public boolean supportsParameter(@NonNull final MethodParameter methodParameter) {
        return methodParameter.getParameterAnnotation(AuthUser.class) != null && methodParameter.getParameterType().equals(User.class);
    }

    @Override
    public Object resolveArgument(@NonNull final MethodParameter methodParameter,
                                  final ModelAndViewContainer modelAndViewContainer,
                                  @NonNull final NativeWebRequest nativeWebRequest,
                                  final WebDataBinderFactory webDataBinderFactory) throws Exception {
        if (!this.supportsParameter(methodParameter))
            return WebArgumentResolver.UNRESOLVED;

        //      required
        final boolean required = Objects.requireNonNull(methodParameter.getParameterAnnotation(AuthUser.class)).required();

        //  HttpServletRequest   
        Optional<HttpServletRequest> httpServletRequestOptional = Optional.ofNullable(nativeWebRequest.getNativeRequest(HttpServletRequest.class));

        //         
        Optional<UserJwtSignature> userJwtSignature =
                this.extractAuthTokenFromRequest(nativeWebRequest, httpServletRequestOptional.orElse(null))
                        .flatMap(tokenHandler::extractTokenInfo)
                        .flatMap(userJwtSignatureService::extractByTokenInfo);
        
        if (required) {
            //        
            if (userJwtSignature.isEmpty() || null == userJwtSignature.get().getUser())
                //       
                throw new AuthenticationException(httpServletRequestOptional.map(HttpServletRequest::getMethod).orElse(null),
                        httpServletRequestOptional.map(HttpServletRequest::getRequestURI).orElse(null));

            final User user = userJwtSignature.get().getUser();

            //    
            return this.appendCurrentSignature(user, userJwtSignature.get());
        } else {
            //    ,     ,  null
            return this.appendCurrentSignature(userJwtSignature.map(UserJwtSignature::getUser).orElse(null),
                    userJwtSignature.orElse(null));
        }
    }

    private User appendCurrentSignature(User user, UserJwtSignature userJwtSignature) {
        Optional.ofNullable(user).ifPresent(u -> u.setCurrentSignature(userJwtSignature));
        return user;
    }

    private Optional<String> extractAuthTokenFromRequest(@NonNull final NativeWebRequest nativeWebRequest,
                                                         final HttpServletRequest httpServletRequest) {
        return Optional.ofNullable(httpServletRequest)
                .flatMap(this::extractAuthTokenFromRequestByCookie)
                .or(() -> this.extractAuthTokenFromRequestByHeader(nativeWebRequest));
    }

    private Optional<String> extractAuthTokenFromRequestByCookie(final HttpServletRequest httpServletRequest) {
        return Optional
                .ofNullable(httpServletRequest)
                .map(request -> WebUtils.getCookie(httpServletRequest, AUTH_COOKIE_NAME))
                .map(Cookie::getValue);
    }

    private Optional<String> extractAuthTokenFromRequestByHeader(@NonNull final NativeWebRequest nativeWebRequest) {
        return Optional.ofNullable(nativeWebRequest.getHeader(AUTH_HEADER_NAME));
    }
}




コンストラクターでは、トークンを渡すことができるCookieとヘッダーの名前を受け入れます。application.propertiesで取り出しました



app.api.tokenKeyName=Auth-Token
app.api.tokenHeaderName=Auth-Token


以前に作成されたTokenHandlerUserJwtSignatureServiceもコンストラクターに渡されます。



IDとトークン署名によるデータベースからのユーザーの標準抽出があるため、UserJwtSignatureServiceは考慮しません。



しかし、ハンドラー自体のコードをより詳細に分析してみましょう。



supportsParameter -かどうかをチェックする方法は、必要な要件を満たしているかどうか。



resolveArgumentはメインメソッドであり、その中ですべての「魔法」が発生します。



では、ここで何が起こっているのでしょうか。



  1. 注釈から必須フィールドの値を取得します
  2. HttpServletRequest
  3. ,
  4. required, , .

    , , ( , ).

    , , , .
  5. , required, , null


注釈プロセッサが作成されました。しかし、それだけではありません。それを知るには、Springに登録する必要があります。ここではすべてが簡単です。SpringのWebMvcConfigurerインターフェイスを実装する構成ファイルを作成しaddArgumentResolversメソッドをオーバーライドします



WebMvcConfig.java
package org.website.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.website.annotation.handler.AuthUserHandlerMethodArgumentResolver;
import org.website.jwt.TokenHandler;
import org.website.service.repository.UserJwtSignatureService;

import java.util.List;

@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {

    @Value("${app.api.tokenKeyName}")
    private String tokenKeyName;

    @Value("${app.api.tokenHeaderName}")
    private String tokenHeaderName;

    private final TokenHandler tokenHandler;
    private final UserJwtSignatureService userJwtSignatureService;

    @Autowired
    public WebMvcConfig(final TokenHandler tokenHandler,
                        final UserJwtSignatureService userJwtSignatureService) {
        this.tokenHandler = tokenHandler;
        this.userJwtSignatureService = userJwtSignatureService;
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new AuthUserHandlerMethodArgumentResolver(
                this.tokenKeyName,
                this.tokenHeaderName,
                this.tokenHandler,
                this.userJwtSignatureService));
    }
}




これで注釈の書き込みは終了です。



5.AuthenticationExceptionの処理



前のセクションの注釈ハンドラーで、コントローラーメソッドに許可が必要であるが、ユーザーが許可されていない場合、AuthenticationExceptionをスローしました



次に、必要な情報を使用してjsonをユーザーに返すために、この例外のクラスを追加して処理する必要があります。



AuthenticationException.java
package org.website.annotation.exception;

public class AuthenticationException extends Exception {

    public AuthenticationException(String requestMethod, String url) {
        super(String.format("%s - %s", requestMethod, url));
    }
}




そして今、例外ハンドラー自体。発生した例外を処理し、ユーザーに標準のSpringエラーページではなく、必要なjsonを提供するために、SpringにはControllerAdviceアノテーションがあります



実行を処理するためのクラスを追加しましょう。



AuthenticationExceptionControllerAdvice.java
package org.website.controller.exception.handler;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.website.annotation.exception.AuthenticationException;
import org.website.http.response.Error;
import org.website.http.response.ErrorResponse;
import org.website.http.response.Response;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@ControllerAdvice
@Slf4j
public class AuthenticationExceptionControllerAdvice extends AbstractControllerAdvice {

    @Value("${app.api.tokenKeyName}")
    private String tokenKeyName;

    @ExceptionHandler({AuthenticationException.class})
    public Response authenticationException(HttpServletResponse response) {
        Cookie cookie = new Cookie(tokenKeyName, "");
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        return new ErrorResponse.Builder(Error.AUTHENTICATION_ERROR).build();
    }
}




これで、AuthenticationExceptionがスローされた場合、それがキャッチされ、AUTHENTICATION_ERRORエラーのあるjsonがユーザーに返されます。



6.コントローラー



さて、実際には、そのためにすべてが開始されました。3つの方法でコントローラーを作成しましょう。



  1. 必須の承認
  2. がない必須承認
  3. 新規ユーザーの登録。最小限のコード。ユーザーをデータベースに保存するだけで、パスワードは保存されません。これにより、新しいユーザーのトークンも返されます


TestAuthController.java
package org.website.controller;

import com.google.gson.JsonObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.website.annotation.AuthUser;
import org.website.domain.User;
import org.website.http.response.Response;
import org.website.http.response.SuccessResponse;
import org.website.jwt.GeneratedTokenInfo;
import org.website.service.repository.UserJwtSignatureService;
import org.website.service.repository.UserService;

import java.util.Optional;

@RestController
@RequestMapping("/test-auth")
public class TestAuthController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserJwtSignatureService userJwtSignatureService;

    @RequestMapping(value = "/required", method = RequestMethod.GET)
    public Response required(@AuthUser final User user) {
        return new SuccessResponse.Builder(user).build();
    }

    @RequestMapping(value = "/not-required", method = RequestMethod.GET)
    public Response notRequired(@AuthUser(required = false) final User user) {
        JsonObject response = new JsonObject();

        if (null == user) {
            response.addProperty("message", "Hello guest!");
        } else {
            response.addProperty("message", "Hello " + user.getFirstName());
        }

        return new SuccessResponse.Builder(response).build();
    }

    @RequestMapping(value = "/sign-up", method = RequestMethod.GET)
    public Response signUp(@RequestParam String firstName) {
        User user = userService.save(User.builder().firstName(firstName).build());

        Optional<GeneratedTokenInfo> generatedTokenInfoOptional =
                userJwtSignatureService.generateNewTokenAndSaveToDb(user);

        return new SuccessResponse.Builder(user)
                .addPropertyToPayload("token", generatedTokenInfoOptional.get().getToken())
                .build();
    }
}




必要notRequired方法、我々は我々の注釈を挿入します。

最初のケースでは、ユーザーが許可されていない場合、jsonはエラーで返され、許可されている場合は、ユーザーに関する情報が返されます。



2番目のケースでは、ユーザーがログインしていない場合、メッセージHello guest!、許可されている場合は、その名前が返されます。

すべてが実際に機能することを確認しましょう。



まず、許可されていないユーザーとして両方の方法を確認しましょう。



/必須




/不要




すべてが期待どおりです。承認が必要な場合はエラーが返され、2番目のケースでは「Helloguest 」というメッセージが返されました..。



次に、登録して同じメソッドを呼び出してみましょう。ただし、リクエストヘッダーでトークンを転送します。



/ サインアップ




応答は、承認が必要な要求に使用できるトークンを返しました。



これを確認しましょう:



/必須




/不要




最初のケースでは、ユーザーに関する情報のみが返されます。2番目のケースでは、ウェルカムメッセージが返されます。



ワーキング!



7.結論



この方法が唯一の正しい解決策であるとは主張していません。誰かがSpringSecurityの使用を好むかもしれません。しかし、冒頭で述べたように、この方法は実証済みで使いやすく、非常にうまく機能します。



All Articles