Search

์‚ฌ์šฉ์ž ์ •์˜ ์ธ์ฆ

์‚ฌ์šฉ์ž ์ •์˜ ์ธ์ฆ

Spring Security 5.x

์ด์ „ ํŽ˜์ด์ง€

์ด์ „ ํŽ˜์ด์ง€ ๋‚ด์šฉ์— ์ด์–ด์„œ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

Code

Preview

1.
๋กœ๊ทธ์ธ ํ™”๋ฉด
2.
๋ฉ”์ธ ํ™”๋ฉด

์ž‘์—… ํ”„๋กœ์„ธ์Šค

1.
ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ
2.
ํ”„๋กœ์ ํŠธ ์„ค์ •
3.
๋„๋ฉ”์ธ
4.
์„œ๋น„์Šค
5.
์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •

Preview

๋กœ๊ทธ์ธ ํ™”๋ฉด

๋ฉ”์ธ ํ™”๋ฉด

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

build.gradle

spring boot 2.x.x
spring security 5.x.x
plugins { id 'java' id 'war' id 'org.springframework.boot' version '2.7.17' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'com.aloha' version = '0.0.1-SNAPSHOT' java { sourceCompatibility = '17' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.3.1' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' 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:2.3.1' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } tasks.named('test') { useJUnitPlatform() }
Java
๋ณต์‚ฌ

ํ”„๋กœ์ ํŠธ ์„ค์ •

application.properties

spring.application.name=form-custom # ๋ฐ์ดํ„ฐ ์†Œ์Šค - 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.form_custom.domain mybatis.mapper-locations=classpath:mybatis/mapper/**/**.xml
Markdown
๋ณต์‚ฌ

๋„๋ฉ”์ธ

โ€ข
Users.java
โ€ข
UserAuth.java
โ€ข
CustomUser.java

Users.java

@Data public class Users { private String id; private String username; private String password; private String name; private String email; private String profile; private Date createdAt; private Date updatedAt; private int enabled; private List<UserAuth> authList; }
Java
๋ณต์‚ฌ

UserAuth.java

@Data public class UserAuth { private String id; 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.login(username); } catch (Exception e) { e.printStackTrace(); } if( user == null ) { throw new UsernameNotFoundException("์‚ฌ์šฉ์ž๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." + username); } // ๐Ÿ” CustomUser โžก UserDetails CustomUser customUser = new CustomUser(user); return customUser; } }
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 public class SecurityConfig { @Autowired private DataSource dataSource; @Autowired private UserDetailServiceImpl userDetailServiceImpl; // ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์„ค์ • ๋ฉ”์†Œ๋“œ @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // โœ… ์ธ๊ฐ€ ์„ค์ • http.authorizeRequests(requests -> requests .antMatchers("/**").permitAll() .anyRequest().permitAll() ); // ๐Ÿ” ํผ ๋กœ๊ทธ์ธ ์„ค์ • // โœ… ์ปค์Šคํ…€ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ http.formLogin(login -> login.loginPage("/login") .loginProcessingUrl("/login")); // โœ… ์‚ฌ์šฉ์ž ์ •์˜ ์ธ์ฆ ์„ค์ • http.userDetailsService(userDetailServiceImpl); // ๐Ÿ”„ ์ž๋™ ๋กœ๊ทธ์ธ ์„ค์ • http.rememberMe(me -> me.key("aloha") .tokenRepository(tokenRepository()) .tokenValiditySeconds(60 * 60 * 24 * 7)); return http.build(); } /** * ๐Ÿ‘ฎโ€โ™‚๏ธ๐Ÿ” ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ด€๋ฆฌ ๋นˆ ๋“ฑ๋ก ๋ฉ”์†Œ๋“œ * JDBC ์ธ์ฆ ๋ฐฉ์‹ * โœ… ๋ฐ์ดํ„ฐ ์†Œ์Šค (URL, ID, PW) - application.properties * โœ… SQL ์ฟผ๋ฆฌ ๋“ฑ๋ก * โญ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ฟผ๋ฆฌ * โญ ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์ฟผ๋ฆฌ * @return */ // @Bean // public UserDetailsService userDetailsService() { // JdbcUserDetailsManager userDetailsManager // = new JdbcUserDetailsManager(dataSource); // // ์‚ฌ์šฉ์ž ์ธ์ฆ ์ฟผ๋ฆฌ // String sql1 = " SELECT username, password, enabled " // + " FROM user " // + " WHERE username = ? " // ; // // ์‚ฌ์šฉ์ž ๊ถŒํ•œ ์ฟผ๋ฆฌ // String sql2 = " SELECT username, auth " // + " FROM user_auth " // + " WHERE username = ? " // ; // userDetailsManager.setUsersByUsernameQuery(sql1); // userDetailsManager.setAuthoritiesByUsernameQuery(sql2); // return userDetailsManager; // } /** * ๐Ÿƒ ์ž๋™ ๋กœ๊ทธ์ธ ์ €์žฅ์†Œ ๋นˆ ๋“ฑ๋ก * โœ… ๋ฐ์ดํ„ฐ ์†Œ์Šค * โญ persistent_logins ํ…Œ์ด๋ธ” ์ƒ์„ฑ create table persistent_logins ( username varchar(64) not null , series varchar(64) primary key , token varchar(64) not null , last_used timestamp not null ); * ๐Ÿ”„ ์ž๋™ ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค * โœ… ๋กœ๊ทธ์ธ ์‹œ * โžก ๐Ÿ‘ฉโ€๐Ÿ’ผ(ID, ์‹œ๋ฆฌ์ฆˆ, ํ† ํฐ) ์ €์žฅ * โœ… ๋กœ๊ทธ์•„์›ƒ ์‹œ, * โžก ๐Ÿ‘ฉโ€๐Ÿ’ผ(ID, ์‹œ๋ฆฌ์ฆˆ, ํ† ํฐ) ์‚ญ์ œ * @return */ @Bean public PersistentTokenRepository tokenRepository() { // JdbcTokenRepositoryImpl : ํ† ํฐ ์ €์žฅ ๋ฐ์ดํ„ฐ ๋ฒ ์ด์Šค๋ฅผ ๋“ฑ๋กํ•˜๋Š” ๊ฐ์ฒด JdbcTokenRepositoryImpl repositoryImpl = new JdbcTokenRepositoryImpl(); // โœ… ํ† ํฐ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ง€์ • // - ์‹œํ๋ฆฌํ‹ฐ๊ฐ€ ์ž๋™ ๋กœ๊ทธ์ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ DB๋ฅผ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. repositoryImpl.setDataSource(dataSource); // persistent_logins ํ…Œ์ด๋ธ” ์ƒ์„ฑ try { repositoryImpl.getJdbcTemplate().execute(JdbcTokenRepositoryImpl.CREATE_TABLE_SQL); } catch (BadSqlGrammarException e) { log.error("persistent_logins ํ…Œ์ด๋ธ”์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค."); } catch (Exception e) { log.error("์ž๋™ ๋กœ๊ทธ์ธ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ค‘ , ์˜ˆ์™ธ ๋ฐœ์ƒ"); } return repositoryImpl; } }
Java
๋ณต์‚ฌ