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);
}
}