JWTの貼り付け


👏 ユーザー認証用にJWTを貼り付けます


JWTをユーザー認証ツールとして選びましたが、以前の使い方からすると、以前私が行っていた内容はほとんど必要ないので、最初から作業を再開する必要があります.満足です...

🔨 セキュリティ設定


先に設定した内容を優先して行います.
以前の設定

🔨 h 2設定の変更


h 2は、以前はh 2バッチを実行するために使用されていたが、springbootカーネルで実行できる.わかりましたが、こんなに早く忘れてしまいました.ううう
server:
  port: 8080

spring:
  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:h2:mem:logindb
    driver-class-name: org.h2.Driver
    username: sa

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
      format_sql: true
      show-sql: true
    defer-datasource-initialization: true

logging:
  level:
    root: info
設定を上記に変更します.以前とは異なり、設定値はh 2:でenable:trueに内蔵されます.このように、サーバの実行時にh 2 dbが内蔵され、通常はテストに使用されます.
Spring 2.5以降では初期運転順序が変更され、この設定を外すとdefer-datasource-initialization: trueでエラーが発生するため、データが変更されます.sqlを書かないか、対応する設定を入れます!

👦 UserEntity


メンバーのデータベースの定義
@Entity
@Table(name = "user")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserEntity {

    @Id
    @Column(name = "user_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

    @Column(name = "username", length = 50, unique = true)
    private String username;

    @Column(name = "password", length = 100)
    private String password;

    @Column(name = "nickname", length = 50)
    private String nickname;

    @Column(name = "activated")
    private boolean activated;

    @ManyToMany
    @JoinTable(
            name = "user_authority",
            joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "user_id")},
            inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "authority_name")})
    private Set<Authority> authorities;
}
UserEntityは次のように定義されています.
@Entity
@Table(name = "authority")
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Authority {

    @Id
    @Column(name = "authority_name", length = 50)
    private String authorityName;
}
パーミッションのテーブルは、パーミッション名のみを持つように定義されます.また、上にjoin columnでuser id値が指定されているため、キー値はuser idとなる.
INSERT INTO USER (USER_ID, USERNAME, PASSWORD, NICKNAME, ACTIVATED) VALUES (1, 'admin', '$2a$08$lDnHPz7eUkSi6ao14Twuau08mzhWrL4kyZGGU5xfiGALO/Vxd5DOi', 'admin', 1);

INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_USER');
INSERT INTO AUTHORITY (AUTHORITY_NAME) values ('ROLE_ADMIN');

INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_USER');
INSERT INTO USER_AUTHORITY (USER_ID, AUTHORITY_NAME) values (1, 'ROLE_ADMIN');
そしてresourceフォルダの下にあるdata.サーバの実行時に情報を挿入するためにsqlファイルを作成します.
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //security 암호화 encoder를 bean으로 등록
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 이므로 기본설정 사용안함. 기본설정은 비인증시 로그인폼 화면으로 리다이렉트 된다.
                .csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증하므로 세션은 필요없으므로 생성안함.
                .and()
                .authorizeRequests() // 다음 리퀘스트에 대한 사용권한 체크
                .antMatchers("/**").permitAll(); // 가입 및 인증 주소는 누구나 접근가능
                //.antMatchers("/v1/**").permitAll() // 가입 및 인증 주소는 누구나 접근가능
                //.anyRequest().hasRole("USER"); // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers(
                        "/h2-console/**",
                        "/favicon.ico"
                );
    }
}
最後に、セキュリティをh 2コンソールとバビーコンソールにアクセスできるように設定します(=現在は必要ありません).

を選択します.
http://localhost:8080/h2-console
アクセスして接続し、正常に動作していることを確認できます.

🔨 JWT設定の追加

jwt:
  header: Authorization
  secret: juno-eats-toy-project-spring-boot-jwt-secret-login-api-juno-eats-toy-project-spring-boot-jwt-secret-login-api
application.ymlにjwtの設定を追加します.
server:
  port: 8080

spring:
  h2:
    console:
      enabled: true

  datasource:
    url: jdbc:h2:mem:logindb
    driver-class-name: org.h2.Driver
    username: sa

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
      format_sql: true
      show-sql: true
    defer-datasource-initialization: true

jwt:
  header: Authorization
  #HS512 알고리즘을 사용할 것이기 때문에 512bit, 즉 64byte 이상의 secret key를 사용해야 한다.
  #echo 'juno-eats-toy-project-spring-boot-jwt-secret-login-api-juno-eats-toy-project-spring-boot-jwt-secret-login-api-secret'|base64
  secret: juno-eats-toy-project-spring-boot-jwt-secret-login-api-juno-eats-toy-project-spring-boot-jwt-secret-login-api-secret
  token-validity-in-seconds: 8460000

logging:
  level:
    root: info
全体の内容はこうです.
jwtライブラリをgradleに追加します.
plugins {
	id 'org.springframework.boot' version '2.6.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id "org.asciidoctor.jvm.convert" version "3.3.2"	//(1) asciidoctor 추가
}

group = 'com.juno'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-validation'	//validated 추가
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
	testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'	//(2) mockMvc에서 restdocs를 사용할 수 있도록 추가
	//jwt 추가
	implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
	runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}

ext {
	snippetsDir = file('build/generated-snippets')	//(3) 빌드시 snippets 파일들이 저장될 저장소
}

test {
	useJUnitPlatform()
	outputs.dir snippetsDir	//(4) test 실행 시 파일을 (3)에서 설정한 저장소에 출력하도록 설정
}

asciidoctor {	//(5) asccidoctor 설정
	dependsOn test
	inputs.dir snippetsDir
}

asciidoctor.doFirst {	//(6) asciidoctor가 실행될 때 docs 하위 파일 삭제
	delete file('src/main/resources/static/docs')
}

bootJar {	//(7) bootJar 시 asciidoctor 종속되고 build하위 스니펫츠 파일을 classes 하위로 복사
	dependsOn asciidoctor
	copy {
		from "${asciidoctor.outputDir}"
		into 'BOOT-INF/classes/static/docs'
	}
}

task copyDocument(type: Copy) {		//(8) from의 파일을 into로 복사
	dependsOn asciidoctor
	from file("build/docs/asciidoc")
	into file("src/main/resources/static/docs")
}

build {		//(9) build 시 copyDocument 실행
	dependsOn copyDocument
}
既存のプロファイルにはjwt構成のみが追加されています.

🔨 Token Providerの作成


Token ProviderはJWTでトークンの作成と有効性の検証を担当します.
@Component
@Slf4j
public class TokenProvider implements InitializingBean {

    private final String secretKey;   //@Value를 통해 yml에 저장된 secret 값을 가져옴, Lombok valid 아님!
    private final Long tokenValidityInMilliseconds; //토큰이 살아있는 시간 (1000L * 60 * 60 = 1시간)

    private Key key;    //afterPropertiesSet()에서 key값을 할당해줌
    private final String AUTH = "auth";

    //생성자에서 secretKey값을 base64로 인코딩하여 값을 넣어주고 토큰의 생명주기도 설정한다.
    public TokenProvider(@Value("${jwt.secret}") String secretKey, @Value("${jwt.token-validity-in-seconds}") Long tokenValidityInMilliseconds) {
        this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
        this.tokenValidityInMilliseconds = tokenValidityInMilliseconds * 1000;
    }

    //bean이 생성되고 의존성 주입이 완료된 다음 init()을 실행하여 base64로 인코딩된 키 값을 decoding하여 key 변수에 할당
    @Override
    public void afterPropertiesSet() throws Exception {
        byte[] keyBytes = Base64.getDecoder().decode(secretKey);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    //Authentication 객체의 권한정보를 이용해서 토큰을 생성하는 createToken 메서드
    public String createToken(Authentication authentication){
        String authorities = authentication.getAuthorities().stream()   //authentication 정보를 가져옴
                .map(GrantedAuthority::getAuthority)                    //GrantedAuthority 객체로 변환 후 getAuthority()
                .collect(Collectors.joining(","));              //배열을 ,로 연결하여서 하나의 String으로 만듦

        Date now = new Date();
        long nowTime = now.getTime();  //현재 시간
        Date validity = new Date(nowTime + this.tokenValidityInMilliseconds);  //현재 시간 + 설정한 토큰 주기를 더해줌

        return Jwts.builder()                               //토큰 생성후 리턴
                .setSubject(authentication.getName())       //토큰 제목
                .setIssuedAt(now)                           //토큰 발행 일자
                .claim(AUTH, authorities)            //담고 싶은 데이터 (key, value) = payload에 들어가는 정보, 여기서는 권한들에 대한 정보를 넣어줌
                .signWith(key, SignatureAlgorithm.HS512)    //key값과 함께 우리가 사용할 암호화 알고리즘 선언
                .setExpiration(validity)                    //토큰 주기 설정
                .compact();
    }

    //token을 파라미터로 받아서 토큰의 정보를 읽어온 후 정보를 확인하여 권한을 반환하는 메서드
    public Authentication getAuthentication(String token){
        Claims claims = Jwts
                .parserBuilder()
                .setSigningKey(key)     //서명한 key 값
                .build()
                .parseClaimsJws(token)  //전달 받은 token
                .getBody();             //parse하여 전달된 값

        Collection<GrantedAuthority> authorities = Arrays.stream(claims.get(AUTH).toString().split(","))    //파싱하여 가져온 claims에서 auth key 값으로 권한들을 가져와서 배열로 만듦
                .map(SimpleGrantedAuthority::new)   //배열을 스트림으로 변환 후 SimpleGrantedAuthority 객체로 변환
                .collect(Collectors.toList());      //List로 만듦

        User principal = new User(claims.getSubject(), "", authorities);    //token에 저장된 제목으로 User 객체 생성

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);  //인터페이스 Authentication의 구현체를 통해 Authentication객체 반환
    }

    //token의 유효성 검사, 각 Exception에 따라 처리 해주면 됨!
    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);  //토큰을 파싱했을 때 정상이 아니라면 exception 발생
            return true;
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("잘못된 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.info("만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.info("지원되지 않는 JWT 토큰입니다.");
        } catch (IllegalArgumentException e) {
            log.info("JWT 토큰이 잘못되었습니다.");
        }
        return false;
    }
}
できるだけ注釈で詳細を書こうと思ったが、理解できるかどうか分からなかった.理解していなければ、大量のコード量から見るのではなく、一つの方法から見ると、大したことはないかもしれません.

🔨 Jwt Customフィルタ

@Slf4j
@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    private final TokenProvider provider;

    //jwt 토큰의 인증 정보를 현재 실행중인 security context에 저장하는 역할을 수행
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String jwt = resolveToken(httpServletRequest);  //request로부터 header에서 토큰 정보를 가져옴
        String requestURI = httpServletRequest.getRequestURI();

        if (StringUtils.hasText(jwt) && provider.validateToken(jwt)) {  //TokenProvider에 작성한 토큰 유효성검사 실행
            Authentication authentication = provider.getAuthentication(jwt);    //토큰에서 Authentication 객체를 반환 받고
            SecurityContextHolder.getContext().setAuthentication(authentication);   //security context에 저장
            log.debug("Security Context에 '{}' 인증 정보를 저장했습니다, uri: {}", authentication.getName(), requestURI);
        } else {
            log.debug("유효한 JWT 토큰이 없습니다, uri: {}", requestURI); //유효성 검사 실패
        }

        chain.doFilter(request, response);
    }

    //reuqest의 header에서 토큰의 정보를 꺼내오기 위한 메서드
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
jwtトークンの認証情報をセキュリティコンテキストに格納するJwtFilterファイルが作成されました.

🔨 Jwt Security Config


JWTSecurityConfigファイルを作成し、上に作成したファイルをSecurityに適用します.
@RequiredArgsConstructor
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    //TokenProvider를 주입
    private final TokenProvider tokenProvider;

    @Override
    public void configure(HttpSecurity http) {
        JwtFilter customFilter = new JwtFilter(tokenProvider);  //JwtFilter를 통해 Security로직에 등록
        http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

🔨 JwtAuthenticationEntryPoint


アクセスしようとすると401 Unautorzedエラーが返されますが、有効な証明書は提供されていません.
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED);    // 유효한 자격증명을 제공하지 않고 접근하려 할때 401
    }
}

🔨 JwtAccessDeniedHandler


必要なパーミッションが存在しない場合は、403 Forbiddenエラーを返します.
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.sendError(HttpServletResponse.SC_FORBIDDEN);   //필요한 권한이 없이 접근하려 할때 403
    }
}

🔨 SecurityConfigのリセット

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  //@PreAuthorize를 메서드 단위로 사용하기 위해 어노테이션 추가
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
    private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

    public SecurityConfig(  //생성자 주입
            TokenProvider tokenProvider,
            JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
            JwtAccessDeniedHandler jwtAccessDeniedHandler
    ) {
        this.tokenProvider = tokenProvider;
        this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
        this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {  //password endcoder
        return new BCryptPasswordEncoder();
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .antMatchers(
                        "/h2-console/**"
                        ,"/favicon.ico"
                        ,"/error"
                );
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // token을 사용하는 방식이기 때문에 csrf를 disable합니다.
                .csrf().disable()

                //security exception handler
                .exceptionHandling()
                .authenticationEntryPoint(jwtAuthenticationEntryPoint)
                .accessDeniedHandler(jwtAccessDeniedHandler)

                // enable h2-console
                .and()
                .headers()
                .frameOptions()
                .sameOrigin()

                // 세션을 사용하지 않기 때문에 STATELESS로 설정
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)

                .and()
                .authorizeRequests()
                .antMatchers("/v1/hello").permitAll()
                .antMatchers("/v1/authenticate").permitAll()
                .antMatchers("/v1/signup").permitAll()
                //그 외 요청은 권한을 가져야함
                .anyRequest().authenticated()
                .and()
                //JwtSecurityConfig 적용
                .apply(new JwtSecurityConfig(tokenProvider));
    }
}
その後、サーバーを実行し、正常に起動すると、まず設定が終了します.
次に、securityとjwtがどのように機能しているかを書いて、この文章に追加しましょう.
以下の文章では、実際に操作されたコインの発行と会員認証を実施します.