스프링 시큐리티 및 JWT 를 활용하기 위해 결국 유저정보를 저장하는 DB 가 있어야 합니다.
아래 글과 겹치는 부분 있을 수 있으니 먼저 확인해보세요. 아래 포스트는 디비 없이 하드코딩으로 user 와 password 를 설정했습니다.
https://juntcom.tistory.com/249
회원 테이블
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN NOT NULL,
authority VARCHAR(50) NOT NULL
);
user 테이블과
authority 권한 테이블을 정규화 해서 나눌 수도 있지만 한 유저당 여러권한이 없다고 보는게 낫고 예시를 위해 한 테이블로 진행하려고 합니다.
Entity Class
@Setter
@Getter
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
private boolean enabled;
@Column(nullable = false)
private String authority;
}
CustomUserDetailsService
UserDetailsService 를 implements 해야 합니다.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(),
user.isEnabled(), true, true, true,
Collections.singletonList(new SimpleGrantedAuthority(user.getAuthority())));
}
}
UserRepository
package com.junt.studybasic.repository;
import com.junt.studybasic.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByUsername(String username);
}
Security
package com.junt.studybasic.config;
import com.junt.studybasic.filter.JwtRequestFilter;
import com.junt.studybasic.service.CustomUserDetailsService;
import com.junt.studybasic.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
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.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
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
public class SecurityConfig {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Login Controller
package com.junt.studybasic.controller;
import com.junt.studybasic.util.JwtUtil;
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.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public String createAuthenticationToken(@RequestBody AuthRequest authRequest) throws Exception {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
);
final UserDetails userDetails = userDetailsService.loadUserByUsername(authRequest.getUsername());
final String jwt = jwtUtil.generateToken(userDetails);
return jwt;
}
}
class AuthRequest {
private String username;
private String password;
// Getters and Setters
}
회원가입 API
UserService
package com.junt.studybasic.service;
import com.junt.studybasic.entity.User;
import com.junt.studybasic.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public User registerNewUser(String username, String password, String authority) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(password));
user.setEnabled(true);
user.setAuthority(authority);
return userRepository.save(user);
}
}
AuthController
package com.junt.studybasic.controller;
import com.junt.studybasic.entity.User;
import com.junt.studybasic.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private UserService userService;
@PostMapping("/register")
public User registerUser(@RequestBody RegisterRequest registerRequest) {
return userService.registerNewUser(
registerRequest.getUsername(),
registerRequest.getPassword(),
registerRequest.getAuthority()
);
}
}
@Getter
@Setter
class RegisterRequest {
private String username;
private String password;
private String authority;
}
JwtUtil
@Component
public class JwtUtil {
private final SecretKey SECRET_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(UserDetails userDetails) {
return createToken(userDetails.getUsername());
}
private String createToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) // 10시간 유효
.signWith(SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
JwtRequestFilter
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@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 = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
참고
유저 권한에 따라 코드를 다르게 실행하는 방법
1. 메서드 수준의 보안 애노테이션 사용
Spring Security는 메서드 수준에서 접근 제어를 지원하는 몇 가지 애노테이션을 제공합니다. 가장 일반적인 애노테이션은 @PreAuthorize와 @Secured입니다.
1.1 @PreAuthorize 애노테이션 사용
@PreAuthorize 애노테이션을 사용하면 메서드 호출 전에 표현식을 평가하여 접근을 제어할 수 있습니다. 이를 위해 @EnableGlobalMethodSecurity(prePostEnabled = true)를 설정해야 합니다.
package com.junt.studybasic.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// 추가 설정이 필요 없다면 이 클래스는 비어 있어도 됩니다.
}
서비스 클래스 예시
package com.junt.studybasic.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class MyService {
@PreAuthorize("hasRole('ROLE_ADMIN')")
public void adminOnlyMethod() {
// 관리자 전용 메서드 로직
}
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
public void userOrAdminMethod() {
// 사용자 또는 관리자 접근 가능 메서드 로직
}
}
2. Spring Security 권한 검사 API 사용
Spring Security의 권한 검사 API를 사용하여 프로그램 내에서 동적으로 권한을 검사할 수도 있습니다.
2.1 SecurityContextHolder를 사용한 권한 검사
@Service
public class MyService {
public void performActionBasedOnRole() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) {
// 관리자 전용 로직
} else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
// 사용자 전용 로직
} else {
// 기타 권한 로직
}
}
}
3. 컨트롤러에서 권한에 따라 다른 로직 수행
Spring MVC 컨트롤러에서도 @PreAuthorize를 사용할 수 있습니다.
3.1 컨트롤러에서의 예시
package com.junt.studybasic.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@GetMapping("/admin")
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String adminEndpoint() {
return "관리자 전용 페이지";
}
@GetMapping("/user")
@PreAuthorize("hasRole('ROLE_USER')")
public String userEndpoint() {
return "사용자 전용 페이지";
}
@GetMapping("/common")
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
public String commonEndpoint() {
return "모든 사용자 접근 가능 페이지";
}
}
요약
• 메서드 수준 보안 애노테이션: @PreAuthorize와 @Secured를 사용하여 메서드 접근 제어.
• 권한 검사 API: SecurityContextHolder를 사용하여 동적으로 권한 검사.
• 컨트롤러에서 권한 검사: @PreAuthorize를 사용하여 요청 핸들러 접근 제어.
• 템플릿 엔진에서 권한 검사: Thymeleaf를 사용하여 동적으로 UI 요소 표시.
'Spring > spring boot 및 기타' 카테고리의 다른 글
spring boot 예외처리 핸들러 @ControolerAdvice (0) | 2024.08.22 |
---|---|
spring security 와 jwt 를 이용한 인증 인가(spring boot3) (0) | 2024.06.17 |
spring webflux mono excel dowonlad(poi 라이브러리) (0) | 2023.06.27 |
spring boot 3 r2dbc 설정(mysql) (0) | 2023.06.23 |
spring netty thread sleep, webflux 에서 sleep 어떻게 하는게 나을까 (0) | 2023.06.14 |