개발자 유미님 영상을 참고하면서 공부한 시리즈임.
@Controller
public class MainController {
@GetMapping("/")
public String mainPage() {
return "main";
}
}
resources/templates/main.mustache
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Main Page</title>
</head>
<body>
Main Page.
</body>
</html>

localhost:8080에 접속해보면 시큐리티로 인해 막혀있음.user와 아래 콘솔창에 뜬 비밀번호를 이용하면 로그인 가능.

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// throws Exception 적는 이유 : authorizeHttpRequests, build가 예외를 던지고 있음.
httpSecurity
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
);
return httpSecurity.build();
}
}
SecurityFilterChain이라는 인터페이스가 반환타입.HttpSecurity 객체를 받음..anyRequest().authenticated()


login.mustache 로그인 페이지.
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
login Page
<hr>
<form action="/loginProc" method="post" name="loginForm">
<input id="username" type="text" name="username" placeholder="id"/>
<input id="password" type="password" name="password" placeholder="password"/>
<input type="submit" value="login"/>
</form>
</body>
</html>
<input>태그로 username, password를 받고 submit을 보냄.POST방식으로 /loginProc로 전송됨.컨트롤러가 필요함.@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage() {
return "login";
}
}

login버튼을 클릭하면 아래와 같이 URL이 /loginProc로 요청을 보냄.
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// throws Exception 적는 이유 : authorizeHttpRequests, build가 예외를 던지고 있음.
httpSecurity
.csrf((csrf) -> csrf.disable()
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
)
.formLogin((formLogin) -> formLogin
.loginPage("/login").loginProcessingUrl("/loginProc").permitAll()
);
return httpSecurity.build();
}
}
.csrf((csrf) -> csrf.disable()
...
.formLogin((formLogin) -> formLogin
.loginPage("/login").loginProcessingUrl("/loginProc").permitAll().csrf((csrf) -> csrf.disable()loginPage를 설정함으로써 /adminURL 같이 특정 권한이 필요한 페이지에 접근하려고 할 경우 자동으로 /login로 리다이렉트 시킴.loginProcessingUrl("/loginProc")/loginProc로 요청이 보내지는데 이걸 스프링 시큐리티가 받아서 권한 처리, 인증 처리를 진행함.
ADMIN권한이 필요한 /admin을 입력한 후 엔터를 눌러서 URL 요청을 보내면 아래와 같이 /login 페이지로 리다이렉트 됨.
스프링 시큐리티는 사용자 인증, 즉 로그인을 하면 비밀번호에 대해 단방향 해시 암호화를 진행한 뒤 저장되어 있는 비밀번호와 대조함.
스프링 시큐리티는 암호화를 위해 BCryptPasswordEncoder를 제공하고 있음.
빈 등록을 해서 사용함.@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// throws Exception 적는 이유 : authorizeHttpRequests, build가 예외를 던지고 있음.
httpSecurity
// CSRF 비활성화.
.csrf((csrf) -> csrf.disable()
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/login").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
)
// 권한이 필요한 URL 요청 시 /login으로 리다이렉트 시킴.
.formLogin((formLogin) -> formLogin
.loginPage("/login").loginProcessingUrl("/loginProc").permitAll()
);
return httpSecurity.build();
}
}
추가된 코드.
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
public class BCryptPasswordEncoder implements PasswordEncoder {
private Pattern BCRYPT_PATTERN;
private final Log logger;
private final int strength;
private final BCryptVersion version;
private final SecureRandom random;
public BCryptPasswordEncoder() {
this(-1);
}
public BCryptPasswordEncoder(int strength) {
this(strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptVersion version) {
this(version, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptVersion version, SecureRandom random) {
this(version, -1, random);
}
public BCryptPasswordEncoder(int strength, SecureRandom random) {
this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
}
public BCryptPasswordEncoder(BCryptVersion version, int strength) {
this(version, strength, (SecureRandom)null);
}
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
this.logger = LogFactory.getLog(this.getClass());
if (strength == -1 || strength >= 4 && strength <= 31) {
this.version = version;
this.strength = strength == -1 ? 10 : strength;
this.random = random;
} else {
throw new IllegalArgumentException("Bad strength");
}
}
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else {
String salt = this.getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
}
private String getSalt() {
return this.random != null ? BCrypt.gensalt(this.version.getVersion(), this.strength, this.random) : BCrypt.gensalt(this.version.getVersion(), this.strength);
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
} else if (encodedPassword != null && encodedPassword.length() != 0) {
if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
this.logger.warn("Encoded password does not look like BCrypt");
return false;
} else {
return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
public boolean upgradeEncoding(String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
Matcher matcher = this.BCRYPT_PATTERN.matcher(encodedPassword);
if (!matcher.matches()) {
throw new IllegalArgumentException("Encoded password does not look like BCrypt: " + encodedPassword);
} else {
int strength = Integer.parseInt(matcher.group(2));
return strength < this.strength;
}
} else {
this.logger.warn("Empty encoded password");
return false;
}
}
public static enum BCryptVersion {
$2A("$2a"),
$2Y("$2y"),
$2B("$2b");
private final String version;
private BCryptVersion(String version) {
this.version = version;
}
public String getVersion() {
return this.version;
}
}
}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://(IP):(Port)/(DB)?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
spring.datasource.username=(username)
spring.datasource.password=(password)
# JPA 쿼리문 확인 가능
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
# JPA Hibernate 콘솔 출력문 가독성 좋게해줌.
spring.jpa.properties.hibernate.format_sql=true
/join요청을 처리할 컨트롤러.@Controller
public class JoinController {
@GetMapping("/join")
public String joinPage() {
return "join";
}
}
/joinProc라는 URL을 POST방식으로 요청함.join.mustache
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Join Page</title>
</head>
<body>
<form action="/joinProc" method="POST" name="joinForm">
<input type="text" name="username" placeholder="Username"/>
<input type="password" name="password" placeholder="Password"/>
<input type="submit" value="Join">
</form>
</body>
</html>
form 데이터)을 POST방식으로 보내기 때문에 @PostMapping으로 처리. @PostMapping("/joinProc")
public String joinProcess() {
....
return "redirect:/login";
}
username, password
JoinDTO
@Getter
@Setter
public class JoinDTo {
private String username;
private String password;
}
JoinController
@PostMapping("/joinProc")
public String joinProcess() {
return "redirect:/login";
}
JoinService
@Service
public class JoinService {
public void JoinProcess(JoinDTo joinDTo) {
....
}
}
JoinController
@PostMapping("/joinProc")
public String joinProcess(JoinDTo joinDTo) {
System.out.println("joinDTo.getUsername() = " + joinDTo.getUsername());
System.out.println("joinDTo.getPassword() = " + joinDTo.getPassword());
joinService.JoinProcess(joinDTo);
return "redirect:/login";
}
UserEntity
@Entity
@Getter
@Setter
public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
private String role;
}
UserRepository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}
JoinService
public void JoinProcess(JoinDTo joinDTo) {
UserEntity userEntity = new UserEntity();
userEntity.setUsername(joinDTo.getUsername());
userEntity.setPassword(bCryptPasswordEncoder.encode(joinDTo.getPassword()));
userEntity.setRole("ROLE_USER");
userRepository.save(userEntity);
}
role의 경우 사용자가 회원가입할 때 선택할 수 없으므로 현재는 서버 로직에서 수동으로 등록.BCryptPasswordEncoder의 encode를 통해 해쉬 암호화를 해서 저장함./join, /joinProcURL을 로그인 하지 않은 사용자, 즉 모든 사용자가 접근할 수 있도록 해야됨.SecurityConfig
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// throws Exception 적는 이유 : authorizeHttpRequests, build가 예외를 던지고 있음.
httpSecurity
// CSRF 비활성화.
.csrf((csrf) -> csrf.disable()
)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/", "/login", "/join", "/loginProc", "/joinProc").permitAll()
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/my/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
)
// 권한이 필요한 URL 요청 시 /login으로 리다이렉트 시킴.
.formLogin((formLogin) -> formLogin
.loginPage("/login").loginProcessingUrl("/loginProc").permitAll()
);
return httpSecurity.build();
}
/join만 추가하고 /joinProc는 추가하지 않았을 경우.


public class UserEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(unique = true)
private String username;
...
}
unique를 줘서 중복된 값이 들어오지 못하도록 함.UserRepository
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
boolean existsByUsername(String username);
}
username을 매개변수로 갖는 boolean 타입의 메서드 추가.public void JoinProcess(JoinDTo joinDTo) {
if (userRepository.existsByUsername(joinDTo.getUsername())) {
return;
}
UserEntity userEntity = new UserEntity();
userEntity.setUsername(joinDTo.getUsername());
userEntity.setPassword(bCryptPasswordEncoder.encode(joinDTo.getPassword()));
userEntity.setRole("ROLE_USER");
userRepository.save(userEntity);
}
service에 있는 joinProcess 메서드에 if문을 추가해서 회원 중복을 방지함.UserDetails, UserDetailsService를 구현해줘야함.package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userRepository.findByUsername(username);
System.out.println("loadUserByUsername");
if (userEntity != null) {
return new CustomUserDetails(userEntity);
}
return null;
}
}
UserRepository를 주입 받음.loadUserByUsername 메서드의 구현부는 매개변수로 받은 username을 이용해서 데이터베이스에 저장되어 있던 데이터와 검증하는 로직을 작성하면 됨.new CustomUserDetails(userEntity);를 리턴.UserDetailsService는 UserDetails객체를 반환하는 인터페이스.loadUserByUsername(String username) 메서드를 구현해야하며 사용자 이름(username)을 기반으로 UserDetails를 생성함.public class CustomUserDetails implements UserDetails {
private UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
};
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
return new CustomUserDetails(userEntity);를 생성하기 위한 생성자 추가.
private UserEntity userEntity;getAuthorities 메서드는 유저의 권한을 리턴하는 메서드.
UserDetails는 Spring Security에서 사용자 정보를 담는 인터페이스.
getAuthorities() 메서드는 사용자가 가진 권한 목록을 반환함.
ADMIN권한을 가지는 유저 생성.



/adminURL 접속시 위와 같이 정상적으로 접속됨.