์ฌ์ฉ์ ์ ์ ์ธ์ฆ
์ด์ ํ์ด์ง
์ด์ ํ์ด์ง ๋ด์ฉ์ ์ด์ด์ ์งํํฉ๋๋ค.
Code
Preview
1.
๋ก๊ทธ์ธ ํ๋ฉด
2.
๋ฉ์ธ ํ๋ฉด
์์ ํ๋ก์ธ์ค
1.
ํ๋ก์ ํธ ์์ฑ
2.
ํ๋ก์ ํธ ์ค์
3.
๋๋ฉ์ธ
4.
์๋น์ค
5.
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
Preview
๋ก๊ทธ์ธ ํ๋ฉด
๋ฉ์ธ ํ๋ฉด
ํ๋ก์ ํธ ์์ฑ
build.gradle
plugins {
id 'java'
id 'war'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'com.aloha'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:3.0.3'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
Java
๋ณต์ฌ
ํ๋ก์ ํธ ์ค์
application.properties
spring.application.name=login
# ๋ฐ์ดํฐ ์์ค - MySQL
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/aloha?serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true&useSSL=false&autoReconnection=true&autoReconnection=true
spring.datasource.username=aloha
spring.datasource.password=123456
# Mybatis ์ค์
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.type-aliases-package=com.aloha.login.domain
mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Markdown
๋ณต์ฌ
๋๋ฉ์ธ
โข
Users.java
โข
UserAuth.java
โข
CustomUser.java
Users.java
@Data
public class Users {
private Long no;
private String id;
private String username;
private String password;
private String name;
private String email;
private Date createdAt;
private Date updatedAt;
private int enabled;
private List<UserAuth> authList;
}
Java
๋ณต์ฌ
UserAuth.java
@Data
public class UserAuth {
private Long no;
private String username;
private String auth;
}
Java
๋ณต์ฌ
CustomUser.java
DB์์ ์กฐํํ ๋ฐ์ดํฐ๋ฅผ ์ง์ ์ ์ํ ์ฌ์ฉ์ ๋ฐ ๊ถํ Users, UserAuth ๊ฐ์ฒด๋ก ๋งคํํ ๊ฒฐ๊ณผ๋ฅผ ์คํ๋ง ์ํ๋ฆฌํฐ์ ์ฌ์ฉ์ ์ ๋ณด ์ธํฐํ์ด์ค์ธ UserDetails ๋ฅผ ๊ตฌํํ์ฌ ์ฌ์ฉ์ ์ ์ ์ธ์ฆ ๊ฐ์ฒด๋ก ์ ์ํฉ๋๋ค.
@Getter
@ToString
public class CustomUser implements UserDetails {
// ์ฌ์ฉ์ DTO
private Users user;
public CustomUser(Users user) {
this.user = user;
}
/**
* ๐ ๊ถํ ์ ๋ณด ๋ฉ์๋
* โ
UserDetails ๋ฅผ CustomUser ๋ก ๊ตฌํํ์ฌ,
* Spring Security ์ User ๋์ ์ฌ์ฉ์ ์ ์ ์ธ์ฆ ๊ฐ์ฒด(CustomUser)๋ก ์ ์ฉ
* โ CustomUser ์ ์ฉ ์, ๊ถํ์ ์ฌ์ฉํ ๋๋ 'ROLE_' ๋ถ์ฌ์ ์ฌ์ฉํด์ผํ๋ค.
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getAuthList().stream()
.map( (auth) -> new SimpleGrantedAuthority(auth.getAuth()) )
.collect(Collectors.toList());
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return user.getEnabled() == 0 ? false : true;
}
}
Java
๋ณต์ฌ
์๋น์ค
โข
UserDetailServiceImpl.java
UserDetailServiceImpl.java
UserDetailsService ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ์ฌ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๋ ๋ฐฉ๋ฒ์ ์ ์ํ ์ ์์ต๋๋ค.
/**
* ๐ UserDetailsService : ์ฌ์ฉ์ ์ ๋ณด ๋ถ๋ฌ์ค๋ ์ธํฐํ์ด์ค
* โ
์ด ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ์ฌ, ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๋ ๋ฐฉ๋ฒ์ ์ ์ํ ์ ์์ต๋๋ค.
*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info(":::::::::: UserDetailServiceImpl ::::::::::");
log.info("- ์ฌ์ฉ์ ์ ์ ์ธ์ฆ์ ์ํด, ์ฌ์ฉ์ ์ ๋ณด ์กฐํ");
log.info("- username : " + username);
Users user = null;
try {
// ๐ฉโ๐ผ ์ฌ์ฉ์ ์ ๋ณด ๋ฐ ๊ถํ ์กฐํ
user = userMapper.select(username);
} catch (Exception e) {
e.printStackTrace();
}
if( user == null ) {
throw new UsernameNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค." + username);
}
// ๐ CustomUser โก UserDetails
CustomUser customUser = new CustomUser(user);
return customUser;
}
}
Java
๋ณต์ฌ
Mapper
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aloha.login.mapper.UserMapper">
<resultMap type="Users" id="userMap">
<id property="no" column="no" />
<result property="no" column="no" />
<result property="id" column="id" />
<result property="username" column="username" />
<result property="password" column="password" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="enabled" column="enabled" />
<result property="createdAt" column="created_at" />
<result property="updatedAt" column="updated_at" />
<collection property="authList" resultMap="authMap"></collection>
</resultMap>
<resultMap type="UserAuth" id="authMap">
<result property="no" column="no" />
<result property="username" column="username" />
<result property="auth" column="auth" />
</resultMap>
<!-- ํ์ ์กฐํ - id -->
<select id="select" resultMap="userMap">
SELECT u.no
,u.username
,password
,name
,email
,enabled
,created_at
,updated_at
,auth
FROM user u LEFT OUTER JOIN user_auth auth ON u.username = auth.username
WHERE u.username = #{username}
</select>
<!-- ํ์ ๊ฐ์
-->
<insert id="join">
INSERT INTO user ( username, password, name, email )
VALUES ( #{username}, #{password}, #{name}, #{email} )
</insert>
<!-- ํ์ ์ ๋ณด ์์ -->
<update id="update">
UPDATE user
SET name = #{name}
,email = #{email}
,updated_at = now()
WHERE username = #{username}
</update>
<!-- ํ์ ๊ถํ ๋ฑ๋ก -->
<insert id="insertAuth">
INSERT INTO user_auth( username, auth )
VALUES ( #{username}, #{auth} )
</insert>
</mapper>
XML
๋ณต์ฌ
UserMapper.java
@Mapper
public interface UserMapper {
// ํ์ ์กฐํ
public Users select(String id) throws Exception;
// ํ์ ๊ฐ์
public int join(Users user) throws Exception;
// ํ์ ์์
public int update(Users user) throws Exception;
// ํ์ ๊ถํ ๋ฑ๋ก
public int insertAuth(UserAuth userAuth) throws Exception;
}
Java
๋ณต์ฌ
์คํ๋ง ์ํ๋ฆฌํฐ ์ค์
โข
SecurityConfig.java
SecurityConfig.java
UserDetailsService ๋น์ ๋ฉ์๋๋ก ์ ์ํ๋ ๋ฐฉ์์์, @Service ํด๋์ค๋ฅผ ์ ์ํ ์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์ ์ ํ๋ ๋ฐฉ์์ผ๋ก ์ ํํ๋ค.
์ด์ ๋ฐฉ์ (AS-IS)
์ ์ฉ ๋ฐฉ์ (TO-BE)
โข
UserDetailServiceImpl ์์กด์ฑ ์ฃผ์
โข
์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์
UserDetailServiceImpl ์์กด์ฑ ์ฃผ์
@Autowired
private UserDetailServiceImpl userDetailServiceImpl;
Java
๋ณต์ฌ
์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์
// โ
์ฌ์ฉ์ ์ ์ ์ธ์ฆ ์ค์
http.userDetailsService(userDetailServiceImpl);
Java
๋ณต์ฌ
SecurityConfig.java
@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) //์ด๋
ธํ
์ด์
์ prePostEnabled = true๋ฅผ ์ถ๊ฐํ๋ฉด AuthenticationManager๋ฅผ ์๋์ผ๋ก ๊ตฌ์ฑํฉ๋๋ค.
public class SecurityConfig {
@Autowired
private CustomUserDetailService customUserDetailService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
private AuthenticationManager authenticationManager;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
log.info("securityFilterChain...");
// ํผ ๊ธฐ๋ฐ ๋ก๊ทธ์ธ ๋นํ์ฑํ
http.formLogin( login -> login.disable() );
// HTTP ๊ธฐ๋ณธ ์ธ์ฆ ๋นํ์ฑํ
http.httpBasic( basic -> basic.disable() );
// CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ ๋ฐฉ์ด ๊ธฐ๋ฅ ๋นํ์ฑํ
http.csrf( csrf -> csrf.disable() );
// ํํฐ ์ค์
// โ
JWT ์์ฒญ ํํฐ 1๏ธโฃ
// โ
JWT ์ธ์ฆ ํํฐ 2๏ธโฃ
http.addFilterAt(new JwtAuthenticationFilter(authenticationManager, jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new JwtRequestFilter(authenticationManager, jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
;
// ์ธ๊ฐ ์ค์
http.authorizeHttpRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
.antMatchers("/user/**").hasAnyRole("USER" , "ADMIN")
.antMatchers("/admin/**").hasRole("ADMIN")
// .anyRequest().authenticated()
;
// ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ถ๋ฌ์ค๋ ์๋น์ค ์ค์
http.userDetailsService(customUserDetailService);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Java
๋ณต์ฌ
์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ ธ์ค๊ธฐ
Principal vs Authentication vs User
์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ฃผ์ ์ธํฐํ์ด์ค ๋ฐ ํด๋์ค
๋ค์์ Spring Security์ ์ฃผ์ ์ธํฐํ์ด์ค์ ํด๋์ค์ ๊ตฌ์กฐ๋์
๋๋ค
java.security.Principal (Interface)
โ
Authentication (Interface)
โ
UsernamePasswordAuthenticationToken (๊ตฌํ์ฒด)
โ
getPrincipal()
โณ org.springframework.security.core.userdetails.User
(implements UserDetails)
Java
๋ณต์ฌ
์ด ๊ตฌ์กฐ๋๋ Spring Security์์ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด๊ฐ ์ด๋ป๊ฒ ๊ณ์ธตํ๋์ด ์๋์ง๋ฅผ ๋ณด์ฌ์ค๋๋ค.
Principal์ ์ต์์ ์ธํฐํ์ด์ค์ด๋ฉฐ, Authentication์ ์ด๋ฅผ ํ์ฅํ์ฌ ๋ ๋ง์ ์ธ์ฆ ๊ด๋ จ ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค. UsernamePasswordAuthenticationToken์ Authentication์ ๊ตฌํ์ฒด ์ค ํ๋์ด๋ฉฐ, getPrincipal() ๋ฉ์๋๋ฅผ ํตํด UserDetails๋ฅผ ๊ตฌํํ User ๊ฐ์ฒด๋ฅผ ๋ฐํํฉ๋๋ค.
java.security.Principal (Interface)
โ
Authentication (Interface)
โ
UsernamePasswordAuthenticationToken (๊ตฌํ์ฒด)
โ
getPrincipal()
โณ org.springframework.security.core.userdetails.User
โณ com.aloha.security6.domain.CustomUser
(implements UserDetails)
: Users (์ฌ์ฉ์ ์ ๋ณด ๊ฐ์ฒด)
Java
๋ณต์ฌ
public String home(@AuthenticationPrincipal CustomUser authUser, Model model) throws Exception {
if( authUser != null ) {
log.info("authUser : " + authUser);
Users user = authUser.getUser();
model.addAttribute("user", user);
}
return "index";
}
Java
๋ณต์ฌ
์ด์ ์ธ์
์ ๋ฑ๋ก๋ CustomUser ๊ฐ์ฒด์์ Users ๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์ ๋ฑ๋ก๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ๋ก ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค.
ํด๋์ค/์ธํฐํ์ด์ค | ์ญํ | ์ฃผ์ ๋ฉ์๋ | ์ฃผ์ ๊ตฌํ์ฒด |
Principal | ์ธ์ฆ๋ ์ฌ์ฉ์์ ๊ธฐ๋ณธ ์ ๋ณด ์ ๊ณต (ํ์ค ์๋ฐ ์ธํฐํ์ด์ค) | getName() | Spring Security์ User, ๋๋ ์ฌ์ฉ์ ์ ์ ๊ฐ์ฒด |
Authentication | Spring Security ์ธ์ฆ ์ ๋ณด (์ฌ์ฉ์, ๊ถํ, ์ธ์ฆ ์ํ ๋ฑ ํฌํจ) | getPrincipal(), getAuthorities(), isAuthenticated() | UsernamePasswordAuthenticationToken, JwtAuthenticationToken |
User | ์ธ์ฆ๋ ์ฌ์ฉ์์ ์ธ๋ถ ์ ๋ณด ์ ๊ณต (Spring Security์ ๊ตฌํ์ฒด, UserDetails๋ฅผ ๊ตฌํ) | getUsername(), getAuthorities(), isAccountNonLocked() ๋ฑ | Spring Security ๊ธฐ๋ณธ User ํด๋์ค |
โข
Principal: ์ต์ํ์ ์ฌ์ฉ์ ์๋ณ ์ ๋ณด๋ฅผ ์ ๊ณต.
โข
Authentication: ์ธ์ฆ ๋ฐ ๊ถํ ๊ด๋ จ ์ ๋ณด๋ฅผ ๋ชจ๋ ํฌํจ.
โข
User: Spring Security์์ ์ฌ์ฉ์ ์ธ๋ถ ์ ๋ณด๋ฅผ ์ ๊ณตํ๋ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ์ฒด.