[Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (2)
이번에는 이전 글에 이어서 Spring-Security를 구현해보려고 한다.
조금 이번 글부터는 난이도가 있어지니깐 개발 전략을 잘 숙지해서 작업하면 좋겠다.
1. [Spring-Framework] 16. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (1), 2020-09-27
https://yyman.tistory.com/1422
14. 개발 전략
하나 만들면 다 되는 게 아니다. 계속 연속해서 복합적으로 수정작업을 해줘야 한다.
그래서 개발 작업이 힘이 든다. 쉽지만 않다.
그림 24. 개발 전략도
SecurityWebApplicationInitializer.java, SecurityConfig.java는 Spring Security 구현에 있어서 핵심이라고 해도 무방하다.
두 개를 잘 구현한다면, 셈플 로그인 페이지는 볼 수 있다.
문제는 자바 버전으로 구현했을 때 보안 토큰 절차가 xml방식에 비해서 매우 까다롭게 반응한다는 것이다.
그래서 간단한 코드로 태스트를 해보기도 전에 DB 설계를 할 수 밖에 없었다.
이유는 토큰 인증 때문에 그렇다.
SqlMapSessionFactory도 계속 반복해서 다양한 영역에서 재사용될 것이다.
15. config의 SecurityWebApplicationInitializer.java (필수 파일)
이 파일을 보면, 제일 황당한 생각이 들 수 밖에 없는 이유가 코드는 몇 줄 안 되는데, 없으면 동작이 안 된다는 것이다.
package com.springMVC.javaSecurity5.config;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}
파일명: SecurityWebApplicationInitializer.java
[첨부(Attachments)]
SecurityWebApplicationInitializer.zip
16. config의 SecurityConfig.java (필수 파일)
"SecurityConfig.java" 이 파일도 없으면 Spring Security with 자바 버전이 동작되지 않는다.
패키지 경로: com.springMVC.javaSecurity5.config
package com.springMVC.javaSecurity5.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.web.filter.CharacterEncodingFilter;
import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
private CustomAuthenticationProvider authProvider;
protected void configure(AuthenticationManagerBuilder auth, HttpSecurity http) throws Exception {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
/* UTF-8 한글 보완 */
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
http.addFilterBefore(filter,CsrfFilter.class);
auth
.authenticationProvider(authProvider);
/* 현재 - 임시
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")
.and()
.withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");
*/
// withUser("admin").password(passwordEncoder.encode("1234")).roles("USER", "ADMIN");
/* 임시
UserBuilder users = User.withDefaultPasswordEncoder();
auth.inMemoryAuthentication()
.withUser(users.username("admin").password("1234").roles("USER"))
.withUser(users.username("user").password("1234").roles("ADMIN"));
*/
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// index
.antMatchers("/")
.permitAll()
// 접근 오류
.antMatchers("/member/accessDenied")
.permitAll()
.antMatchers("/member/accessDeniedView")
.permitAll()
// 회원 로그인 기능
.antMatchers("/member/loginForm")
.permitAll()
// 관리자 페이지 기능
.antMatchers("/admin/**")
.hasRole("ADMIN")
// "RULE_ADMIN이라고 DB에 입력되어 있다면, RULE_은 제거하고 입력해야 인식함."
// 폼 로그인 명세
.and()
.formLogin()
.permitAll()
.loginPage("/member/loginForm")
.failureForwardUrl("/member/loginForm?error")
.defaultSuccessUrl("/")
.usernameParameter("id")
.passwordParameter("password")
// 로그아웃 처리
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.deleteCookies("JSESSION_ID")
.deleteCookies("remember-me")
// 로그인
.and()
.rememberMe()
.tokenValiditySeconds(604800)
.tokenRepository(persistentTokenRepository())
// 예외처리(
.and()
.exceptionHandling()
.accessDeniedPage("/member/accessDenied")
// csrf 설정
.and()
.csrf().disable();
/*
http.authorizeRequests()
.antMatchers("/login")
.permitAll()
.antMatchers("/**")
.hasAnyRole("ADMIN", "USER")
.hasAnyAuthority("RULE_ADMIN", "RULE_USER")
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/")
.failureUrl("/login?error=true")
.permitAll()
.loginPage("/login")
.usernameParameter("email")
.passwordParameter("password")
.successHandler(successHandler())
.failureHandler(failureHandler())
.permitAll();
.and()
.logout()
.logoutSuccessUrl("/login?logout=true")
.invalidateHttpSession(true)
.permitAll()
.and()
.csrf()
.disable();
*/
}
// 로그아웃 Persistent_Logins에 관한 설정 (주석 해도 무방)...
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
DataSource usrDS = getDataSource();
db.setDataSource(usrDS);
return db;
}
// DataSource 불러오기 (주석 해도 무방)
@Bean
public DataSource getDataSource() {
// BasicDataSource dataSource = new BasicDataSource(); - Apache DBCP2
SqlMapSessionFactory factory = SqlMapSessionFactory.getInstance();
return factory.getOracleDataSource(); // 오라클 적용함.
}
// 비밀번호 생성 - 암호(BCryptPasswordEncoder)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
파일명: SecurityConfig.java
[첨부(Attachments)]
SecurityConfig.zip
비고: 주석 잘 쳐서 정리해서 빌드해보면, 내장 로그인 페이지를 볼 수 있다.
- SecurityWebApplicationInitializer.java, SecurityConfig.java 두 개 파일의 힘이 얼마나 큰지 실감 해볼 수 있다.
Spring-Framework 설정 전체를 제어해버린다고 해도 된다.
17. 로그인 인증 - Spring Security (CustomAuthenticationProvider.java)
이 코드 부분은 찾아보려고 해도 쉽게 나오지 않는다. 어려운 부분 중 하나이다.
공개하는 이유는 삽질을 적게 하라는 의미이다.
패키지 경로: com.springMVC.javaSecurity5.config
package com.springMVC.javaSecurity5.config;
import java.util.List;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import com.springMVC.javaSecurity5.service.CustomUserDetailsService;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDeSer;
@Override
public Authentication authenticate(Authentication authentication) {
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
if ( username.equals("fail")) {
System.out.println("(에러)아이디: 실패");
return null;
}
// DB 정보 읽기
userDeSer = new CustomUserDetailsService();
UserDetails userDetail = userDeSer.loadUserByUsername(username);
@SuppressWarnings("unchecked")
List<GrantedAuthority> roles = (List<GrantedAuthority>) userDetail.getAuthorities();
// 권한
System.out.println("DB불러오기-권한:" + userDetail.getAuthorities());
System.out.println("DB불러오기-비밀번호:" + userDetail.getPassword());
System.out.println("roles:" + roles.get(0));
if ( !matchPassword(password, userDetail.getPassword())) {
System.out.println("(에러)비밀번호: 불일치" + password);
return null;
}
UsernamePasswordAuthenticationToken result =
new UsernamePasswordAuthenticationToken(username, password, roles);
result.setDetails(userDetail);
return result;
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
private boolean matchPassword(String loginPwd, String password) {
BCryptPasswordEncoder secure = new BCryptPasswordEncoder();
return secure.matches(loginPwd, password);
}
}
파일명: CustomAuthenicationProvider.java
[첨부(Attachments)]
CustomAuthenticationProvider.zip
어디에 구체적으로 사용되는 부분인가?
사용하는 영역은 SecurityConfig.java에 http의 auth의 .authenicationProvider()에 사용된다.
그림 25. SecurityConfig.java
왜 이 코드를 사용하는지 소개해본다면,
"No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken"
이 문제가 디버그 오류창에 뜨는 것을 볼 수 있다.
기본 내장형 계정 생성 등으로 인증을 시도하면 xml방식에서는 처리해줬는데, java방식에서는 인증받지 못한다.
/* 현재 - 임시
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder)
.withUser("user").password(passwordEncoder.encode("1234")).roles("RULE_USER")
.and()
.withUser("admin").password(passwordEncoder.encode("1234")).roles("RULE_USER", "RULE_ADMIN");
*/
[문제가 발생되는 기본형 - 코드]
이 코드로 작업하면, xml에서는 동작되었던 부분이 동작되질 않는다.
토큰 인증도 해결할 겸 "CustomAuthenicationProvider.java를 설계해서 문제를 해결한 것이다.
18. SQL (Factory) - SqlMapSessionFactory.java
나중에 CP(Connection Pool, 커넥션 풀)이라고 불리는 것으로 구현해봐도 좋을 듯 싶다.
iBatis를 남용해서 프로젝트에 기본마냥 소개하는 책들이 무척 많은데 기본은 순수한 DB를 사용하는 것부터 출발하는 것이다.
iBatis는 SQL 코드 개발 등에서 생산성이 좋아지는 도구 중 하나이지만, 필수 사항은 아니라고 본다.
차라리 필수 사항을 꼽아본다면, 커넥션 풀을 하나 추천해보고 싶다.
아무튼 커넥션 풀 주제가 아니기 때문에 생략한다.
패키지 경로: com.springMVC.javaSecurity5.db
package com.springMVC.javaSecurity5.db;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import oracle.jdbc.pool.OracleDataSource;
public class SqlMapSessionFactory {
private static SqlMapSessionFactory factory = new SqlMapSessionFactory();
private SqlMapSessionFactory() {}
private final String driverName = "oracle.jdbc.driver.OracleDriver";
private final String dbUrl = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
private final String userName = "{사용자계정명}";
private final String userPassword = "{비밀번호}";
public static SqlMapSessionFactory getInstance() {
return factory;
}
/*
* public static DataSource getMySQLDataSource() {
Properties props = new Properties();
FileInputStream fis = null;
MysqlDataSource mysqlDS = null;
try {
fis = new FileInputStream("db.properties");
props.load(fis);
mysqlDS = new MysqlDataSource();
mysqlDS.setURL(props.getProperty("MYSQL_DB_URL"));
mysqlDS.setUser(props.getProperty("MYSQL_DB_USERNAME"));
mysqlDS.setPassword(props.getProperty("MYSQL_DB_PASSWORD"));
} catch (IOException e) {
e.printStackTrace();
}
return mysqlDS;
}
*/
public DataSource getOracleDataSource(){
OracleDataSource oracleDS = null;
try {
oracleDS = new OracleDataSource();
oracleDS.setURL(dbUrl);
oracleDS.setUser(userName);
oracleDS.setPassword(userPassword);
} catch (SQLException e) {
e.printStackTrace();
}
return oracleDS;
}
public Connection connect() {
Connection conn = null;
try {
Class.forName(driverName);
conn = DriverManager.getConnection(dbUrl, userName, userPassword);
}
catch(Exception ex) {
System.out.println("오류 발생: " + ex);
}
return conn;
}
public void close(Connection conn, PreparedStatement ps, ResultSet rs) {
if ( rs != null ) {
try {
rs.close();
}
catch(Exception ex) {
System.out.println("오류 발생: " + ex);
}
close(conn, ps); // Recursive 구조 응용(재귀 함수)
} // end of if
}
public void close(Connection conn, PreparedStatement ps) {
if (ps != null ) {
try {
ps.close();
}
catch(Exception ex) {
System.out.println("오류 발생: " + ex);
}
} // end of if
if (conn != null ) {
try {
conn.close();
}
catch(Exception ex) {
System.out.println("오류 발생: " + ex);
}
} // end of if
}
}
파일명: SqlMapSessionFactory.java
[첨부(Attachments)]
SqlMapSessionFactory.zip
19. Model - CustomUserDetails.java
순수한 Model 형태는 아니고, 부분 개량하였다.
Spring-Security에서 제공하는 UserDetails(인터페이스)를 Model 클래스에 구현해야 한다.
List<role> 기능 문제 등으로 인해서 String authorities를 List<GrantedAuthority>로 변경하였다.
@Override 된 부분들이 UserDetails에 정의된 내용이다.
패키지 경로: com.springMVC.javaSecurity5.model
package com.springMVC.javaSecurity5.model;
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;
public class CustomUserDetails implements UserDetails{
private static final long serialVersionUID = 1L;
private String username;
private String password;
// 개량함. (다중 권한 고려)
private List<GrantedAuthority> authorities;
private boolean enabled;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthority(String authority) {
// 권한 객체 생성
if ( authorities == null ) {
authorities = new ArrayList<GrantedAuthority>();
}
// 권한 추가
SimpleGrantedAuthority grantObj = new SimpleGrantedAuthority(authority);
authorities.add(grantObj);
}
public boolean getEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isAccountNonLocked() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO Auto-generated method stub
return false;
}
@Override
public boolean isEnabled() {
// TODO Auto-generated method stub
return false;
}
}
파일명: CustomUserDetails.java
[첨부(Attachments)]
CustomUserDetails.zip
20. Service - CustomUserDetailsService.java
CustomUserDetailsService는 Spring-Security의 "UserDetailsService(인터페이스)"로 정의된 내용을 구현하는 것이다.
인터페이스의 영향도 있지만, DB를 실제로 불러올 때 사용자 관점에서 처리되는 부분이라고 본다.
package com.springMVC.javaSecurity5.service;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.springMVC.javaSecurity5.dao.SqlSessionTemplate;
import com.springMVC.javaSecurity5.model.CustomUserDetails;
public class CustomUserDetailsService implements UserDetailsService {
private SqlSessionTemplate sqlSession = SqlSessionTemplate.getInstance();
public CustomUserDetails getUserById(String username) {
return sqlSession.selectOne("user.selectUserById", username);
}
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
CustomUserDetails user = sqlSession.selectOne("null", username);
if(user==null) {
throw new UsernameNotFoundException(username);
}
return user;
}
}
파일명: CustomUserDetailsService.java
[첨부(Attachments)]
CustomUserDetailsService.zip
21. DAO - SqlSessionTemplate.java
실제 DB를 구현하는 부분이다.
package com.springMVC.javaSecurity5.dao;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import com.springMVC.javaSecurity5.db.SqlMapSessionFactory;
import com.springMVC.javaSecurity5.model.CustomUserDetails;
public class SqlSessionTemplate {
private SqlSessionTemplate() {}
private static SqlSessionTemplate sqlTemplate;
private static SqlMapSessionFactory session;
public static SqlSessionTemplate getInstance(){
if(sqlTemplate == null){
sqlTemplate = new SqlSessionTemplate();
session = SqlMapSessionFactory.getInstance();
}
return sqlTemplate;
}
// 추후 iBatis 고려
public CustomUserDetails selectOne(String id, String username) {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
CustomUserDetails node = null;
String sql = "select g1.username, g1.password, g2.authority, " +
"g1.enabled from comp_users g1, comp_authorities g2 where g1.username = g2.username " +
"and g1.username = ?";
System.out.println(sql);
try {
conn = session.connect();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, username);
rs = pstmt.executeQuery();
while ( rs.next() ) {
// 데이터가 존재할 때, 노드 생성
node = new CustomUserDetails();
node.setUsername(rs.getNString(1));
node.setPassword(rs.getNString(2));
node.setAuthority(rs.getNString(3));
System.out.println("rs:" + rs.getNString(3));
node.setEnabled(rs.getBoolean(4));
}
}catch(Exception ex) {
System.out.println("오류 발생: " + ex);
}
finally {
session.close(conn, pstmt, rs);
}
return node;
}
}
파일명: SqlSessionTemplate.java
[첨부(Attachments)]
SqlSessionTemplate.zip
* 3부에서는 View에 대해서 구현하는 방법을 소개하겠다.
3부에서는 "jsp 파일" 등 사용자 인터페이스 화면에 대해서 소개하겠다.
1. [Spring-Framework] 17. Spring MVC, Spring Security 5.4, Oracle - 보안처리(로그인-Java) (3), 2020-09-27
https://yyman.tistory.com/1424