AWS Back Day 72. "Spring Boot와 MySQL을 사용한 보안, 사용자 인증 및 권한 부여"

이강용·2023년 4월 13일
0

Spring Boot

목록 보기
7/20

Security

MVN Repository > jjwt 검색

  • JJWT::API , JJWT::Impl , JJWT:: Jackson 설치

MVN Repository > spring boot security 검색

MySQL Workbench 접속

Table 생성

user_mst

role_mst

authority_mst

query문 작성

select
		*
from
	user_mst um
    left outer join authority_mst am on(am.user_id = um.user_id)
    left outer join role_mst rm on(rm.role_id = am.role_id)

entity > User , Role , Authority Class 생성

User

package com.web.study.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
	private int user_id;
	private String username;
	private String password;
	private String name;
	private String email;
}

Authority

package com.web.study.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Authority {
	private int authority_id;
	private int user_id;
	private int role_id;
	
}

Role

package com.web.study.domain.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Role {
	private int role_id;
	private String role_name;
	
}

repository > UserRepository interface 생성

UserRepository

package com.web.study.repository;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import com.web.study.domain.entity.Authority;
import com.web.study.domain.entity.User;

@Mapper
public interface UserRepository {
	
	public int saveUser (User user);
	public int addAuthorities(List<Authority> authorities); 
}

mappers > user.xml 생성

user.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.web.study.repository.UserRepository">
	<insert id="saveUser" 
		parameterType="com.web.study.domain.entity.User"
		useGeneratedKeys="true"
		keyProperty="user_id">
		insert into user_mst
		value(0, #{username}, #{password}, #{name}, #{email})
	</insert>
	
	<insert id="addAuthorities" parameterType="list">
		insert into authority_mst
		values
		<foreach collection="list" item="authority" separator=",">
			(0,#{authority.user_id},#{authority.role_id})
		</foreach>
	</insert>
  

</mapper>

service > AuthService 인터페이스 생성

package com.web.study.service;

import com.web.study.dto.request.auth.RegisteUserReqDto;

public interface AuthService {
	
	public void registeUser(RegisteUserReqDto registeUserReqDto );
	
	}
}

dto > request > auth 패키지 > RegisteUserReqDto

package com.web.study.dto.request.auth;

import com.web.study.domain.entity.User;

import lombok.Data;

@Data
public class RegisteUserReqDto {
	private String username;
	private String password;
	private String name;
	private String email;
	
	
	public User toEntity() {
		return User.builder()
				.username(username)
				.password(password)
				.name(name)
				.email(email)
				.build();
	}
}

PASSWORD 암호화

package com.web.study.dto.request.auth;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import com.web.study.domain.entity.User;

import lombok.Data;

@Data
public class RegisteUserReqDto {
	private String username;
	private String password;
	private String name;
	private String email;
	
	
	public User toEntity() {
		return User.builder()
				.username(username)
				.password(new BCryptPasswordEncoder().encode(password))
				.name(name)
				.email(email)
				.build();
	}
}

http://localhost:8080/login

service > AuthServiceImpl 생성

package com.web.study.service;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

import com.web.study.domain.entity.Authority;
import com.web.study.domain.entity.User;
import com.web.study.dto.request.auth.RegisteUserReqDto;
import com.web.study.repository.UserRepository;

import lombok.RequiredArgsConstructor;


@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

	
	
	private final UserRepository userRepository;
	
	@Override
	public void registeUser(RegisteUserReqDto registeUserReqDto) {
		
		User userEntity = registeUserReqDto.toEntity();
		userRepository.saveUser(userEntity); // insert user_mst
		
		List<Authority> authorities = new ArrayList<>();
		authorities.add(Authority.builder().user_id(userEntity.getUser_id()).role_id(1).build());
		
		userRepository.addAuthorities(authorities);
	}

}

config > SecurityConfig 생성

package com.web.study.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;




@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
	// security filter
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();
		http.authorizeRequests()
			.antMatchers("/auth/register/**", "/auth/login/**")
			.permitAll()
			.anyRequest()
			.authenticated();
	}
}

CSRF란 무엇인가?

  • CSRF(Cross - Site Request Forgery는 웹 사이트의 취약점을 이용하여 사용자가 신뢰할 수 있는 사이트에서 악의적인 요청을 보내도록 하는 공격 방법, 사용자의 인증 정보를 이용하여 공격자가 사용자 몰래 원하지 않는 행위를 수행하게 만드는데, 이과정에서 사용자의 인증 토큰, 쿠키 등을 이용함

  • 웹 사이트는 이러한 CSRF 공격을 막기 위해 다양한 방법을 사용

    • 요청을 전송할 때 CSRF 토큰을 포함하여 서버가 이 토큰을 검증함으로써 정상적인 요청인지 확인하는 방법을 사용.
    • Spring Security는 CSRF 공격을 방어하기 위한 기능이 기본적으로 활성화되어 있음

controller > auth(패키지) > AuthController

package com.web.study.controller.auth;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.web.study.dto.DataResponseDto;
import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.auth.LoginReqDto;
import com.web.study.dto.request.auth.RegisteUserReqDto;
import com.web.study.service.AuthService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AuthController {
	
	private final AuthService authService;
	
	
	@PostMapping("/auth/register")
	public ResponseEntity<? extends ResponseDto> registe(@RequestBody RegisteUserReqDto registeUserReqDto) {
		authService.duplicatedUsername(registeUserReqDto);
		authService.registeUser(registeUserReqDto);
		return ResponseEntity.ok().body(ResponseDto.ofDefault());
		
	}
	
	@PostMapping("/auth/login")
	public ResponseEntity<? extends ResponseDto> login(@RequestBody LoginReqDto loginReqDto) {
		
		authService.login(loginReqDto);
		return ResponseEntity.ok().body(DataResponseDto.of(authService.login(loginReqDto)));
		
	
	}
}

postman 응답 확인

{
    "username" : "abc",
    "password" : "1234",
    "name" : "김준일",
    "email" : "abc@gmail.com"
}

DB에 회원가입 추가

user.xml 추가

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.web.study.repository.UserRepository">

	<resultMap type="com.web.study.domain.entity.Role" id="role">
		<result column="role_id" property="role_id"/>
		<result column="role_name" property="role_name"/>
	</resultMap>
	
	<resultMap type="com.web.study.domain.entity.Authority" id="authority">
		<result column="authority_id" property="authority_id"/>
		<result column="user_id" property="user_id"/>
		<result column="role_id" property="role_id"/>
		<collection property="role" resultMap="role"/>
	</resultMap>
	
	<resultMap type="com.web.study.domain.entity.User" id="user">
		<result column="user_id" property="user_id"/>
		<result column="username" property="username"/>
		<result column="password" property="password"/>
		<result column="name" property="name"/>
		<result column="email" property="email"/>
		<collection property="authorities" javaType="list" resultMap="authority"></collection>
	</resultMap>


	<insert id="saveUser" 
		parameterType="com.web.study.domain.entity.User"
		useGeneratedKeys="true"
		keyProperty="user_id">
		insert into user_mst
		value(0, #{username}, #{password}, #{name}, #{email})
	</insert>
	
	<insert id="addAuthorities" parameterType="list">
		insert into authority_mst
		values
		<foreach collection="list" item="authority" separator=",">
			(0,#{authority.user_id},#{authority.role_id})
		</foreach>
	</insert>
	<select id="findUserByUsername" resultMap="user">
		select
			um.user_id,
			um.username,
			um.password,
			um.name,
			um.email,
			
			am.authority_id,
			am.role_id,
			
			rm.role_name
		from
			user_mst um
			left outer join authority_mst am on(am.user_id = um.user_id)
			left outer join role_mst rm on(rm.role_id = am.user_id)
		where
			um.username = #{username}
	</select>
  

</mapper>

AuthServiceImpl 수정

package com.web.study.service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.web.study.domain.entity.Authority;
import com.web.study.domain.entity.User;
import com.web.study.dto.request.auth.RegisteUserReqDto;
import com.web.study.exception.CustomException;
import com.web.study.repository.UserRepository;

import lombok.RequiredArgsConstructor;


@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

	
	
	private final UserRepository userRepository;
	
	@Override
	public void registeUser(RegisteUserReqDto registeUserReqDto) {
		
		User userEntity = registeUserReqDto.toEntity();
		userRepository.saveUser(userEntity); // insert user_mst
		
		List<Authority> authorities = new ArrayList<>();
		authorities.add(Authority.builder().user_id(userEntity.getUser_id()).role_id(1).build());
		
		userRepository.addAuthorities(authorities);
	}

	@Override
	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto) {
		
		User userEntity = userRepository.findUserByUsername(registeUserReqDto.getUsername());
		if(userEntity != null) {
			Map<String, String> errorMap = new HashMap<>();
			errorMap.put("username", "이미 사용중인 사용자이름입니다.");
			
			throw new CustomException("중복 검사 오류", errorMap);
			
		}
	}

}

AuthController 수정

package com.web.study.controller.auth;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.web.study.dto.ResponseDto;
import com.web.study.dto.request.auth.RegisteUserReqDto;
import com.web.study.service.AuthService;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class AuthController {
	
	private final AuthService authService;
	
	
	@PostMapping("/auth/register")
	public ResponseEntity<? extends ResponseDto> registe(@RequestBody RegisteUserReqDto registeUserReqDto) {
		authService.duplicatedUsername(registeUserReqDto);
		authService.registeUser(registeUserReqDto);
		return ResponseEntity.ok().body(ResponseDto.ofDefault());
		
	}
}

결과

AuthController 수정

코드를 입력하세요

dto > request > auth > LoginReqDto

package com.web.study.dto.request.auth;

import lombok.Data;

@Data
public class LoginReqDto {
	private String username;
	private String password;
}

AuthService 수정

package com.web.study.service;

import com.web.study.dto.request.auth.LoginReqDto;
import com.web.study.dto.request.auth.RegisteUserReqDto;
import com.web.study.dto.response.auth.JwtTokenRespDto;

public interface AuthService {
	
	public void registeUser(RegisteUserReqDto registeUserReqDto );
	public void duplicatedUsername(RegisteUserReqDto registeUserReqDto);
	
	
	public  JwtTokenRespDto login(LoginReqDto loginReqDto);
}

dto > response > auth > JwtTokenRespDto

package com.web.study.dto.response.auth;

import lombok.Builder;
import lombok.Data;

@Builder
@Data
public class JwtTokenRespDto {
	
	private String grantType;
	private String accessToken;
}

study > security(패키지 생성) > PrincipalDetailsService (클래스 생성)

package com.web.study.security;

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.web.study.repository.UserRepository;

import lombok.RequiredArgsConstructor;


@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

		
		private final UserRepository userRepository;
	
		@Override
		public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
			
			System.out.println("로그인 시도 감지");
			System.out.println("username: " + username);
			
			
			
			return null;
		}
		
		
}

POSTMAN 로그인 post요청

UserDetailsService의 loadUserByUsername() 호출

package com.web.study.security;

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.web.study.domain.entity.User;
import com.web.study.exception.CustomException;
import com.web.study.repository.UserRepository;

import lombok.RequiredArgsConstructor;


@Service
@RequiredArgsConstructor
public class PrincipalDetailsService implements UserDetailsService {

		
		private final UserRepository userRepository;
	
		@Override
		public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
			
			User userEntity = userRepository.findUserByUsername(username);
			
			if(userEntity == null) {
				throw new CustomException("사용자 정보를 다시 확인해주세요.");
			}
			
			
			return null;
		}
		
		
}

등록되지않은 id 검색 시

entity > User 수정

package com.web.study.domain.entity;

import java.util.ArrayList;
import java.util.List;

import com.web.study.security.PrincipalUserDatails;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {
	private int user_id;
	private String username;
	private String password;
	private String name;
	private String email;
	
	
	private List<Authority> authorities;
	
	
	public PrincipalUserDatails toPrincipal() {
		
		List<String> roles = new ArrayList<>();
		
		authorities.forEach(authority ->{
			roles.add(authority.getRole().getRole_name());
		});
		
		return PrincipalUserDatails.builder()
				.userId(user_id)
				.username(username)
				.password(password)
				.roles(roles)
				.build();
	}
}

security > PrincipalUserDetails 생성

package com.web.study.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;

@Getter
@Builder
public class PrincipalUserDetails implements UserDetails {
	
	package com.web.study.security;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;
import lombok.Getter;


@Builder
@Getter
public class PrincipalUserDetails implements UserDetails {
	
	private static final long serialVersionUID = 2482068462613165077L;
	
	private int userId;
	private String username;
	private String password;
	private List<String> roles;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<SimpleGrantedAuthority> authority = new ArrayList<>();
		
		roles.forEach(role -> {
			authority.add(new SimpleGrantedAuthority(role));
		});
		
		return authority;
	}

	@Override
	public String getPassword() {
		
		return password;
	}

	@Override
	public String getUsername() {
		
		return username;
	}
	
	// 사용 기간 만료
	@Override
	public boolean isAccountNonExpired() {
		
		return true;
	}
	
	/// 계정을 잠궈버림
	@Override
	public boolean isAccountNonLocked() {
		
		return true;
	}
	
	// 비밀번호 5회 틀렸을 때
	@Override
	public boolean isCredentialsNonExpired() {
		
		return true;
	}
	
	// 계정 비활성 상태(이메일 인증을 완료해야하거나, 전화번호 인증을 하지 않았을 때)
	@Override
	public boolean isEnabled() {
		
		return true;
	}
}

security > jwt > JwtTokenProvider 생성

secret key generator

application.yml 추가

jwt:
  secretKey : pwGPHexO93sRZzfKNlMg1cLsHJHoRb66pwGPHexO93sRZzfKNlMg1cLsHJHoRb66

JwtTokenProvider

package com.web.study.security.jwt;

import java.security.Key;
import java.util.Date;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import com.web.study.dto.response.auth.JwtTokenRespDto;
import com.web.study.security.PrincipalUserDetails;

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

@Component
public class JwtTokenProvider {
	private final Key key;
	
	
	public JwtTokenProvider(@Value("${jwt.secretKey}") String secretKey) {
		byte[] keyBytes = Decoders.BASE64.decode(secretKey);
		this.key = Keys.hmacShaKeyFor(keyBytes);
	}
	
	public JwtTokenRespDto creatToken(Authentication authentication) {
		
		StringBuilder authoritiesBuilder = new StringBuilder();
		
		authentication.getAuthorities().forEach(grantedcAuthority -> { 
			
			authoritiesBuilder.append(grantedcAuthority.getAuthority());
			authoritiesBuilder.append(",");
		});
		
		authoritiesBuilder.delete(authoritiesBuilder.length() - 1, authoritiesBuilder.length());
		
		String authorities = authoritiesBuilder.toString();
		
		long now = (new Date()).getTime();
		// 1000 == 1초
		Date tokenExpiresDate = new Date(now + (1000 * 60 * 300)); // 토큰 만료 시간 ( 현재 시간 + Timer)
		
		PrincipalUserDetails userDetails = (PrincipalUserDetails) authentication.getPrincipal();
		
		String accessToken = Jwts.builder()
				.setSubject(authentication.getName())
				.claim("userId", userDetails.getUserId())
				.claim("auth", authorities)
				.setExpiration(tokenExpiresDate)
				.signWith(key, SignatureAlgorithm.HS256)
				.compact();
					
		
		return JwtTokenRespDto.builder()
				.grantType("Bearer")
				.accessToken(accessToken)
				.build();
	}
	
}
profile
HW + SW = 1

0개의 댓글