Spring BootでSecurityとJWTの統合

Spring Bootフレームワークを使用して、SecurityとJWTを統合し、ログイン、データアクセス認証、JWTによるログアウト、APIのロール制御、APIの権限制御を実装します。

使用技術: Spring Boot, Redis, Security, JWT

主要なコード:

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>3.10.0</version>
    </dependency>
</dependencies>

WebSecurityConfig クラス 主な用途:

  • パスワードの暗号化方式を指定: BCryptPasswordEncoder
  • ログイン認証フィルターを指定: JwtLoginFilter
  • APIアクセス認証フィルターを指定: JwtFilter
  • ユーザー情報取得のカスタムサービスを指定: UserService
package com.example.config;

import com.example.constant.JWTConstants;
import com.example.filter.JwtFilter;
import com.example.filter.JwtLoginFilter;
import com.example.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated().and()
                .addFilterBefore(new JwtLoginFilter(JWTConstants.JWT_LOGIN_URL, authenticationManager()), UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(new JwtFilter(), UsernamePasswordAuthenticationFilter.class)
                .csrf()
                .disable();
    }
}

ログイン認証クラス

認証:ここではユーザー名とパスワードを取得して指定されたAPIを呼び出します。

認証成功:JWTに基づいてトークンを生成し、呼び出し元に返します。

認証失敗:エラーメッセージを返します。

package com.example.filter;

import com.example.constant.SystemConstants;
import com.example.entities.User;
import com.example.util.JwtUtils;
import com.example.util.ResponseUtil;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {

    public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = request.getParameter(SystemConstants.LOGIN_INPUT_PARAM_USERNAME);
        String password = request.getParameter(SystemConstants.LOGIN_INPUT_PARAM_PASSWORD);
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
        User userDetails = (User) authResult.getPrincipal();
        String jwt = JwtUtils.getInstance().generateToken(userDetails.getUsername(), userDetails.getRole(), userDetails.getPermissions());
        ResponseUtil.success(response, jwt);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        ResponseUtil.fail(response);
    }
}

APIアクセス認証

白リストにないすべてのAPIをインターセプトします。

リクエストヘッダーからJWTトークンを取得し、その有効性を確認します。

package com.example.filter;

import com.example.constant.AuthExceptionMessage;
import com.example.constant.JWTConstants;
import com.example.util.JwtUtils;
import com.example.util.ResponseUtil;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public class JwtFilter extends GenericFilterBean {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        String jwtToken = req.getHeader(JWTConstants.JWT_REQUEST_HEADER_KEY);
        try {
            UserDetails user = JwtUtils.getInstance().decode(jwtToken);
            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(token);
        } catch (Exception e) {
            e.printStackTrace();
            ResponseUtil.responseJson(response, AuthExceptionMessage.INVALID_TOKEN);
        }
        chain.doFilter(req, response);
    }
}

カスタムユーザー情報取得サービス

package com.example.service;

import org.springframework.security.core.userdetails.UserDetailsService;

public interface UserService extends UserDetailsService {
}
package com.example.service.impl;

import com.example.entities.User;
import com.example.service.UserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = new User();
        user.setUsername(username);
        user.setPassword("$2a$10$Zr6M2iK41CbW0wKLweuilu7PGaCgCF24TD16w7IvTiNkcRE1ZlxM.");
        user.setId("1");
        user.setRole("dev");
        String permissions = "auth:test:test1,ROLE_" + user.getRole();
        user.setPermissions(permissions);
        return user;
    }
}

JWTTOKEN生成と検証ユーティリティクラス

package com.example.util;

import com.example.constant.JWTConstants;
import com.example.entities.User;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.joda.time.DateTime;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class JwtUtils {

    private static final JwtUtils instance = new JwtUtils();

    private JwtUtils() {
    }

    public static JwtUtils getInstance() {
        return instance;
    }

    Algorithm getAlgorithm() {
        return Algorithm.HMAC256(JWTConstants.JWT_KEY);
    }

    public String generateToken(String username, String role, String permission) {
        return JWT.create()
                .withClaim(JWTConstants.JWT_KEY_USER_NAME, username)
                .withClaim(JWTConstants.JWT_KEY_ROLE, role)
                .withClaim(JWTConstants.JWT_KEY_PERMISSION, permission)
                .withExpiresAt(DateTime.now().plusDays(JWTConstants.EXPIRE_DAY).toDate())
                .sign(getAlgorithm());
    }

    public User decode(String token) {
        DecodedJWT decodedJWT = JWT.require(getAlgorithm()).build().verify(token);
        Map<String, Claim> claims = decodedJWT.getClaims();
        String username = claims.get(JWTConstants.JWT_KEY_USER_NAME).asString();
        String role = claims.get(JWTConstants.JWT_KEY_ROLE).asString();
        String permission = claims.get(JWTConstants.JWT_KEY_PERMISSION).asString();
        return new User(username, role, permission);
    }
}

テスト用APIの作成

権限注釈を使用するには、@EnableGlobalMethodSecurity(prePostEnabled = true) を追加します(WebSecurityConfigクラスに追加しています)。

package com.example.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {

    @PreAuthorize("hasPermission('auth','auth:test:test1')")
    @RequestMapping("test1")
    public String test() {
        return "test1::success";
    }

    @PreAuthorize("hasRole('dev')")
    @RequestMapping("test2")
    public String test2() {
        return "test2::success";
    }

    @RequestMapping("test3")
    public String test3() {
        return "test3::success";
    }
}
hasPermission注解也需要自定义处理逻辑
package com.example.util;

import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.Set;
import java.util.stream.Collectors;

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
    @Override
    public boolean hasPermission(Authentication authentication, Object accessType, Object permission) {
        if (authentication != null && authentication.getAuthorities() != null) {
            Set<String> permissionSet = authentication.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
            if (accessType instanceof String) {
                return permissionSet.contains(permission);
            }
        }
        return false;
    }

    @Override
    public boolean hasPermission(Authentication authentication, Serializable serializable, String targetType, Object permission) {
        return hasPermission(authentication, targetType, permission);
    }
}

タグ: Spring Boot Spring Security JWT redis BCryptPasswordEncoder

8月6日 07:19 投稿