原
2024.08.06
10
こんにちは!原です!
今回は JWT トークンを使った認証認可の実装を行っていきます。
build.gradle
ファイルに jjwt
ライブラリを追加します。
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
implementation 'org.glassfish.jaxb:jaxb-runtime:2.3.1'
src/main/java/com/example/user/security
に JwtUtils
を作成します。
JWT の生成、パース、検証を行うためのユーティリティクラスを作成します。
package com.example.user.security;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JwtUtils {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration.ms}")
private long expirationMs;
// JWT からユーザー名を取得
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// JWT から有効期限を取得
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
// JWT からクレーム(claims)を取得
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
// JWT から全てのクレームを取得
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret.getBytes(Charset.forName("UTF-8")))
.parseClaimsJws(token).getBody();
}
// JWT が有効かどうかをチェック
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
// JWT を生成
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
// JWT を生成(クレームを追加可能)
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expirationMs))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
// JWT のバリデーション
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
src/main/java/com/example/user/security
に JwtRequestFilter
を作成します。
package com.example.user.security;
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtils.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtils.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
chain.doFilter(request, response);
}
}
src/main/java/com/example/user/security
に JwtAuthenticationEntryPoint
を作成します。
認証エラー時のエントリーポイントをカスタムで作成します。
package com.example.user.security;
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
src/main/java/com/example/user/security
に SecurityConfig
ファイルを作成します。
package com.example.user.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtRequestFilter jwtRequestFilter, JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/authenticate", "/register").permitAll()
.requestMatchers("/users/**").hasRole("USER")
.anyRequest().authenticated()
)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
}
application.properties
ファイルに、JWT の秘密鍵と有効期限を設定します。
jwt.secret=mysecretkey
jwt.expiration.ms=3600000
src/main/java/com/example/user/security
に AuthenticationRequest
ファイルを作成します。
package com.example.user.security;
public class AuthenticationRequest {
private String username;
private String password;
// Getters and Setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
src/main/java/com/example/user/entity
に Role
クラスを作成します。
package com.example.user.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
username, password, roles フィールドを追加します。
(getter/setter も忘れずに)
package com.example.user.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Users {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String username; // 追加
private String password; // 追加
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
src/main/java/com/example/user/repository
に RoleRepository
クラスを作成します。
package com.example.user.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.user.entity.Role;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
}
src/main/java/com/example/user/security
に AuthenticationController
ファイルを作成します。
JWT トークンを生成するための認証コントローラーを作成します。
package com.example.user.security;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.example.user.entity.Role;
import com.example.user.entity.Users;
import com.example.user.repository.RoleRepository;
import com.example.user.repository.UsersRepository;
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UsersRepository usersRepository;
@Autowired
private RoleRepository roleRepository;
@PostMapping("/authenticate")
public String createAuthenticationToken(@RequestBody AuthenticationRequest authenticationRequest) throws Exception {
authenticate(authenticationRequest.getUsername(), authenticationRequest.getPassword());
final UserDetails userDetails = userDetailsService.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtUtils.generateToken(userDetails);
jwtUtils.extractUsername(token);
return token;
}
@PostMapping("/register")
public String registerUser(@RequestBody Users user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
// ロールを設定
Role userRole = roleRepository.findByName("USER");
if (userRole == null) {
userRole = new Role();
userRole.setName("USER");
roleRepository.save(userRole);
}
Set<Role> roles = new HashSet<>();
roles.add(userRole);
user.setRoles(roles);
usersRepository.save(user);
return "User registered successfully";
}
private void authenticate(String username, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
throw new Exception("INVALID_CREDENTIALS", e);
}
}
}
src/main/java/com/example/user/repository
の UsersRepository
に
findByUsername
メソッドを追加します。
package com.example.user.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.example.user.entity.Users;
public interface UsersRepository extends JpaRepository<Users, Long> {
Users findByUsername(String username); // 追加
}
JwtUserDetailsService
をロール情報に対応するように修正します。
package com.example.user.security;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.user.entity.Users;
import com.example.user.repository.UsersRepository;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = usersRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
List<SimpleGrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList());
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
}
}
src/main/java/com/example/user/repository
の JwtUserDetailsService
を作成します。
Spring Security 用のユーザーサービスを実装します。
package com.example.user.security;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.user.entity.Users;
import com.example.user.repository.UsersRepository;
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
private UsersRepository usersRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users user = usersRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
return new org.springframework.security.core.userdetails.User(user.getName(), user.getPassword(), new ArrayList<>());
}
}
schema.sql を以下のように修正します。
CREATE SEQUENCE IF NOT EXISTS "ID_SEQ"
MINVALUE 1
MAXVALUE 999999999
INCREMENT BY 1
START WITH 2
NOCACHE
NOCYCLE;
CREATE TABLE IF NOT EXISTS users(
id VARCHAR(50) DEFAULT nextval('ID_SEQ') PRIMARY KEY ,
name VARCHAR(50),
email VARCHAR(80),
username VARCHAR(80),
password VARCHAR(80)
);
CREATE TABLE IF NOT EXISTS role (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
CREATE TABLE IF NOT EXISTS users_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE
);
data.sql を以下のように変更します。
INSERT INTO users(id, name, email, username, password) VALUES('1', 'Hara', 'hara@example.com', 'hara', null);
INSERT INTO role(id, name) VALUES(1, 'USER');
これで動くと思ったら、全部 401 になるので、次回動くように修正します!