Spring Security + JWT認証のやり方の備忘録です
まずは、build.gardleのdependenciesの中に下記を追記します
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security' //Spring Security
implementation 'io.jsonwebtoken:jjwt-api:0.11.5' //JWT
testImplementation 'org.springframework.boot:spring-boot-starter-security-test' //Spring Security
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5', 'io.jsonwebtoken:jjwt-jackson:0.11.5'//JWT
}
ユーザーのモデルを作成します。
任意の場所でLoginUser.javaというファイルを作成し、中身を下記のよう編集します。
LoginUser.java
package com.project.backend.domain;
import jakarta.persistence.Column;//JPAのパッケージ使用
import jakarta.persistence.Entity;//JPAのパッケージ使用
import jakarta.persistence.GeneratedValue;//JPAのパッケージ使用
import jakarta.persistence.GenerationType;//JPAのパッケージ使用
import jakarta.persistence.Id;//JPAのパッケージ使用
@Entity
public class LoginUser {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(nullable=false,updatable=false)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private String role;
//コンストラクタ
public LoginUser(){}
public LoginUser(String username, String email, String password, String role){
super();
this.username = username;
this.email = email;
this.password = password;
this.role = role;
}
public Long getId(){
return id;
}
public void setId(Long id){
this.id = id;
}
public String getUsernamr(){
return username;
}
public void setUsernamr(String username){
this.username = username;
}
public String getEmail(){
return email;
}
public void setEmail(String email){
this.email = email;
}
public String getPassword(){
return password;
}
public void setPassword(String password){
this.password = password;
}
public String getRole(){
return role;
}
public void setRole(String role){
this.role = role;
}
}
ユーザーを検索するためのリポジトリを作成します。
LoginUserRepository.java
package com.project.backend.domain;
import java.util.Optional;//null以外の値が含まれている場合も含まれていない場合もあるコンテナ・オブジェクト
import org.springframework.data.repository.CrudRepository;
public interface LoginUserRepository extends CrudRepository<LoginUser, Long>{
Optional<LoginUser> findByUsername(String username);
Optional<LoginUser> findByEmail(String email);
}
UserDetailsServiceImpl
package com.project.backend.service;
import java.util.Optional;
import org.springframework.security.core.userdetails.User.UserBuilder;
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.project.backend.domain.LoginUser;
import com.project.backend.domain.LoginUserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
private final LoginUserRepository repository;
public UserDetailsServiceImpl(LoginUserRepository repository){
this.repository = repository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException{
Optional<LoginUser> user = repository.findByUsername(username);
UserBuilder builder = null;
if(user.isPresent()){
LoginUser currentUser = user.get();
builder = org.springframework.security.core.userdetails.User.withUsername(username);
builder.password(currentUser.getPassword());
builder.roles(currentUser.getRole());
} else{
throw new UsernameNotFoundException("ユーザーが見つかりません");
}
return builder.build();
}
}
Jwtの作成と認証の確認をするクラスを作成します
JwtService.java
package com.project.backend.service;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Date;
@Component
public class JwtService {
static final long EXPIRATIONTIME = 86400000; //1day
static final String PREFIX = "Bearer";
static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String getToken(String username){
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATIONTIME))
.signWith(key).compact();
return token;
}
public String getAuthUser(HttpServletRequest request){
String token = request.getHeader(HttpHeaders.AUTHORIZATION);
if(token != null){
String user = Jwts.parserBuilder().setSigningKey(key)
.build().parseClaimsJws(token.replace(PREFIX, "")).getBody()
.getSubject();
if(user != null)
return user;
}
return null;
}
}
レコードを作成します。
AccountCredentails.java
package com.project.backend.domain;
public record AccountCredentails(String username, String password) {
}
コントローラを作成します。
LoginController.java
package com.project.backend.web;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.project.backend.domain.AccountCredentails;//レコード
import com.project.backend.service.JwtService;
@RestController
public class LoginController {
private final JwtService jwtService;
private final AuthenticationManager authenticationManager;
public LoginController(JwtService jwtService,AuthenticationManager authenticationManager){
this.jwtService = jwtService;
this.authenticationManager = authenticationManager;
}
@PostMapping("/login")
public ResponseEntity<?> getToken(@RequestBody AccountCredentails credentails) {
UsernamePasswordAuthenticationToken creds = new UsernamePasswordAuthenticationToken(credentails.username(),credentails.password());
Authentication auth = authenticationManager.authenticate(creds);
String jwts = jwtService.getToken(auth.getName());
return ResponseEntity.ok()
.header(HttpHeaders.AUTHORIZATION, "Bearer" + jwts)
.header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Authorization")
.build();
}
}
SecurityConfig.javaファイルを作成して、次のように編集します。
SecurityConfig.java
package com.project.backend;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
// import org.springframework.security.authentication.ProviderManager;
// import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.http.HttpMethod;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
// import org.springframework.security.core.userdetails.UserDetails;
// import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
// import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
// import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
// import org.springframework.security.crypto.password.PasswordEncoder;
import com.project.backend.service.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
public SecurityConfig(UserDetailsServiceImpl userDetailsService){
this.userDetailsService = userDetailsService;
}
public void configureGlobal (AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public PasswordEncoder passwordEngcorder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception{
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable())
.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(HttpMethod.POST,"/login").permitAll().anyRequest().authenticated());
return http.build();
}
}
フィルターを作る
AuthenticationFilter.java
package com.project.backend;
//コントローラに送られる前、クライアント側にデータを返す前のフィルター動作
import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import com.project.backend.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class AuthenticationFilter extends OncePerRequestFilter{
private final JwtService jwtService;
//コンストラクタ
public AuthenticationFilter(JwtService jwtService){
this.jwtService =jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws ServletException, java.io.IOException{
String jws = request.getHeader(HttpHeaders.AUTHORIZATION);
if(jws != null){
String user = jwtService.getAuthUser(request);
Authentication authentication = new UsernamePasswordAuthenticationToken(user,null,java.util.Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
SecurityConfig.javaに、このフィルタを適応します
SecurityConfig.java
package com.project.backend;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.http.HttpMethod;
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.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import com.project.backend.service.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationFilter authenticationFilter;// 自作のフィルタ
public SecurityConfig(UserDetailsServiceImpl userDetailsService,AuthenticationFilter authenticationFilter){
this.userDetailsService = userDetailsService;
this.authenticationFilter = authenticationFilter;
}
public void configureGlobal (AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Bean
public PasswordEncoder passwordEngcorder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception{
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable())
.sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests.requestMatchers(HttpMethod.POST,"/login")
.permitAll().anyRequest().authenticated())
.addFilterBefore(authenticationFilter,UsernamePasswordAuthenticationFilter.class);//リクエスト前後で認証されてるかのフィルタ
return http.build();
}
}
こんな感じです。
やや大雑把な説明なので、わかりやすくブラッシュアップしていきます。