Springboot 로그인을 구현해보자. 여기서는 jwt를 이용한 로그인을 구현한다
다음 순서로 진행한다.
1. spring security, jwt gradle 설정
2. property에 시크릿키 설정
3. Jwt util 클래스 작성
4. UserDetailService 작성
5. Jwt 인증 컨트롤러 작성
6. jwt request, response 작성
7. jwt request 필터 작성
8. jwt 인증 엔트리 포인트 작성
9. 스프링 시큐리티 설정
10. 유저 관련 클래스
11. json web token 생성
12. json web tocker 검증하기
1. spring security, jwt gradle 설정
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.2'
2. property에 시크릿키 설정
spring:
h2:
console:
enabled: true
path: /h2-console
port: 9082
profiles:
active: local
datasource:
hikari:
jdbc-url: jdbc:h2:./data/testdb
driver-class-name: org.h2.Driver
username: sa
password:
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
jwt:
secret: jwtsecret
logging:
level:
root: debug
org.hibernate.SQL: debug
3. Jwt util 클래스 작성
@Service
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -798416586417070603L;
private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60;
@Value("${jwt.secret}")
private String secret;
/**
* jwt 토큰에서 username 검색
* @param token
* @return
*/
public String getUsernameFromToken(String token){
try{
return getClaimFromToken(token, Claims::getSubject);
}catch(Exception ex){
throw new UsernameFromTokenException("username from token exception");
}
}
/**
* jwt 토큰에서 날짜 만료 검색
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token){
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
/**
* secret 키를 가지고 토큰에서 정보 검색
* @param token
* @return
*/
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
/**
* 토큰 만료 체크
* @param token
* @return
*/
private Boolean isTokenExpired(String token){
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* username으로 토큰생성
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails){
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
/**
* 토큰을 생성하는 동안
* 1. 토큰, Issuer, Expiration, Subject, ID로 claims를 정의한다.
* 2. HS512알고리즘과 secret key를 가지고 JWT를 서명한다.
* 3. JWS Compact Serialization (https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-3.1)에
* 따라 JWT를 URL 안전 문자열로 압축
* @param claims
* @param username
* @return
*/
private String doGenerateToken(Map<String, Object> claims, String username) {
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY * 1000))
.signWith(SignatureAlgorithm.HS512,secret)
.compact();
}
/**
* 토큰 검증
* @param token
* @param userDetails
* @return
*/
public Boolean validateToken(String token, UserDetails userDetails){
final String username = getUsernameFromToken(token);
return(username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
4. UserDetailService 작성
@Service
public class JwtUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> userOptional = userService.findUserByEmail(username);
User user = userOptional.orElseThrow(()->new UsernameNotFoundException("user name not found!"));
return new org.springframework.security.core.userdetails.User(user.getEmail(),user.getPassword(),new ArrayList<>());
}
}
5. Jwt 인증 컨트롤러 작성
@RestController
@CrossOrigin
public class JwtAuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private JwtUserDetailsService userDetailService;
@RequestMapping(value = "/api/v1/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> createAuthenticationToken(@RequestBody JwtRequest authenticationRequest) throws Exception{
authenticate(authenticationRequest.getUsername(),authenticationRequest.getPassword());
final UserDetails userDetails = userDetailService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
ApiResponse response = new ApiResponse();
response.setData(token);
return new ResponseEntity<>(response, HttpStatus.OK);
}
private void authenticate(String username, String password){
try{
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password));
}catch (DisabledException ex){
throw new DisabledException("USER_DISABLED",ex);
}catch (BadCredentialsException ex){
throw new BadCredentialsException("INVALID_CREDENTIALS",ex);
}
}
}
6. jwt request, response 작성
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class JwtRequest implements Serializable {
private static final long serialVersionUID = 263011851808996064L;
private String username;
private String password;
}
@Getter
@AllArgsConstructor
public class JwtResponse implements Serializable {
private static final long serialVersionUID = -868765630229614668L;
private final String jwttoken;
}
7. jwt request 필터 작성
Bearer에 토큰이 있는지 체크하고 있으면 유효한지 확인후에 해당 url을 호출한다.
@Slf4j
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private JwtUserDetailsService jwtUserDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken =null;
// JWT 토큰은 "Beare token"에 있다. Bearer단어를 제거하고 토큰만 받는다.
if(requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")){
jwtToken = requestTokenHeader.substring(7);
try{
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException ex){
log.error("Unable to get JWT token", ex);
} catch (ExpiredJwtException ex){
log.error("JWT Token has expired", ex);
throw new ExpiredJwtException("JWT Token has expired");
} catch (UsernameFromTokenException ex) {
log.error("token valid error:" + ex.getMessage() ,ex);
throw new UsernameFromTokenException("Username from token error");
} catch (Exception ex) {
log.error("token valid error:" + ex.getMessage() ,ex);
throw new RuntimeException("11 Username from token error");
}
}else{
log.warn("JWT token does not begin with Bearer String");
}
// 토큰을 가져오면 검증을 한다.
if(username != null && SecurityContextHolder.getContext().getAuthentication() == null){
UserDetails userDetails = this.jwtUserDetailsService.loadUserByUsername(username);
// 토큰이 유효한 경우 수동으로 인증을 설정하도록 스프링 시큐리티를 구성한다.
if(jwtTokenUtil.validateToken(jwtToken,userDetails)){
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
// 컨텍스트에 인증을 설정 한 후 현재 사용자가 인증되도록 지정한다.
// 그래서 Spring Security 설정이 성공적으로 넘어간다.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
filterChain.doFilter(request,response);
}
}
8. jwt 인증 엔트리 포인트 작성
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
private static final long serialVersionUID = 4858836244833364014L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
9. 스프링 시큐리티 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserDetailsService jwtUserDetailsService;
@Autowired
private ExceptionHandlerFilter exceptionHandlerFilter;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
public void configGlobal(AuthenticationManagerBuilder auth) throws Exception{
// 일치하는 자격증명을 위해 사용자를 로드할 위치를 알수 있도록
// AuthenticationManager를 구성한다.
// BCryptPasswordEncoder를 이용
auth.userDetailsService(jwtUserDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.cors().disable()
.csrf().disable()
// 이 요청은 인증을 하지 않는다.
.authorizeRequests().antMatchers(
"/authenticate",
"/api/v1/user",
"/api/v1/authenticate")
.permitAll()
// 다른 모든 요청은 인증을 한다.
.anyRequest().authenticated().and()
// 상태없는 세션을 이용하며, 세션에 사용자의 상태를 저장하지 않는다.
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.formLogin().disable()
.headers().frameOptions().disable();
// 모든 요청에 토큰을 검증하는 필터를 추가한다.
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(exceptionHandlerFilter, JwtRequestFilter.class);
}
}
10. 유저 관련 클래스
User.java
@Setter
@Getter
@Entity(name = "user")
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
@NotEmpty @Email @Size(min = 5, max = 255)
private String email;
@NotEmpty @Size(min = 10, max = 100)
private String password;
@NotEmpty @Size(min = 2, max = 100)
private String name;
@Builder
public User(Long id, String email, String password,String name){
this.id = id;
this.email = email;
this.password = password;
this.name = name;
}
}
SignVo.java
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SignVo {
String email;
String password;
String name;
}
UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
public Optional<User> findByEmail(String email);
}
UserService.java
@Service
public class UserService {
UserRepository repository;
PasswordEncoder passwordEncoder;
@Autowired
public UserService(UserRepository repository, PasswordEncoder passwordEncoder){
this.repository = repository;
this.passwordEncoder = passwordEncoder;
}
public void save(User user){
Optional<User> aleadyUser = repository.findByEmail(user.getEmail());
if( aleadyUser.isPresent()){
throw new EmailDuplicateException("email duplicated",ErrorCode.EMAIL_DUPLICATION);
}
user.setPassword(passwordEncoder.encode(user.getPassword()));
repository.save(user);
}
public Optional<User> findUserByEmail(String email){
Optional<User> aleadyUser = repository.findByEmail(email);
return aleadyUser;
}
}
UserController.java
@RestController
public class UserController {
@Autowired
UserService userService;
@PostMapping("/api/v1/user")
@ResponseBody
public ResponseEntity<ApiResponse> saveUser(@RequestBody SignVo signVo){
User user = User.builder()
.email(signVo.getEmail())
.password(signVo.getPassword())
.name(signVo.getName())
.build();
userService.save(user);
return new ResponseEntity<>(new ApiResponse(), HttpStatus.OK);
}
@GetMapping("/api/v1/exist_user/{email}")
public boolean findUserByEmail(@PathVariable String email){
Optional<User> user = userService.findUserByEmail(email);
return user.isPresent() ? true : false;
}
}
ApiResponse.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
private int status = 200;
private String message = "OK";
private String code = "200";
private Object data = "no data";
}
유저 등록하기
11. json web token 생성
12. json web tocken 검증하기
참조: www.javainuse.com/spring/boot-jwt
'웹개발 > 스프링부트' 카테고리의 다른 글
ResponseEntity에 header (httpOnly, secure, cookie ) 세팅하고 json 응답 리턴하기 (0) | 2021.03.18 |
---|---|
스프링부트 profile설정(front 및 api 서버 url 설정하기) (0) | 2021.03.18 |
스프링부트 서블릿 필터 Exception 핸들링 (2) | 2021.03.15 |
Springboot Exception Handling(스프링부트 exception 핸들링) (0) | 2021.03.10 |
스프링 부트에 H2 DB 적용하기 (0) | 2021.03.03 |