헤르메스 LIFE

[Spring Boot] Spring Boot + JWT ( JSON Web Token ) + DB 연결 본문

Spring Boot Framework

[Spring Boot] Spring Boot + JWT ( JSON Web Token ) + DB 연결

헤르메스의날개 2021. 1. 14. 02:10
728x90

개발환경

Spring Boot 2.2.4

JDK 1.8.0_202

REST

Postman : REST Client

 

목표

1. Spring Boot REST 환경

2. Log4j2 추가

3. JWT + Spring Security 를 통한 인증

4. DB 연결 ( Hibernate 제거 )을 통한 사용자 인증


어쩌다 제가 원하는 프로젝트를 찾았습니다. 소스까지 오픈 해주셔서 제 입맛에 맞게 수정을 했습니다.

출처 : daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/

 

SpringBoot2로 Rest api 만들기(8) – SpringSecurity 를 이용한 인증 및 권한부여 - 아빠프로그래머의 좌충

이번 시간에는 SpringSecurity를 이용하여 api 서버의 사용 권한을 제한하는 방법에 대해 알아보도록 하겠습니다. 지금까지 개발한 api는 권한 부여 기능이 없어 누구나 회원 정보를 조회, 생성 및 수

daddyprogrammer.org

아래의 내용은 위 소스를 바탕으로 수정한 내용입니다.


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.rest</groupId>
    <artifactId>SimpleRestAPI</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <!--
                 스프링 부트에서 Log4j2를 사용하기위해선 내부로깅에서 쓰이는 의존성을 제외해주어야 합니다.
                 기본적으로 Spring은 Slf4j라는 로깅 프레임워크를 사용합니다.
                 구현체를 손쉽게 교체할 수 있도록 도와주는 프레임 워크입니다.
                 Slf4j는 인터페이스고 내부 구현체로 logback을 가지고 있는데,
                 Log4j2를 사용하기 위해 exclude 해야 합니다.
                 -->
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!-- Log4j2 로깅 구현체를 사용을 위해 Spring Boot Starter에서 지원해주는  log4j2 의존성을 추가 해준다. -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.modelmapper</groupId>
            <artifactId>modelmapper</artifactId>
            <version>2.3.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <!-- MyBatis 설정-->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>org.bgee.log4jdbc-log4j2</groupId>
            <artifactId>log4jdbc-log4j2-jdbc4.1</artifactId>
            <version>1.16</version>
        </dependency>
        <!-- Database - MSSQL -->
        <dependency>
            <groupId>com.microsoft.sqlserver</groupId>
            <artifactId>mssql-jdbc</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Database - H2 -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Security and JWT -->
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.6.1</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.6.1</version>
        </dependency>
        <!-- yaml-resource-bundle -->
        <dependency>
            <groupId>net.rakugakibox.util</groupId>
            <artifactId>yaml-resource-bundle</artifactId>
            <version>1.1</version>
        </dependency>
        <!-- com.google.code.gson -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
    </dependencies>

    <!--  빌드 플러그인 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
package com.rest.api.controller.v1;

import com.rest.api.entity.User;
import com.rest.api.model.response.CommonResult;
import com.rest.api.model.response.SingleResult;
import com.rest.api.service.SignService;
import com.rest.comm.advice.exception.CEmailSigninFailedException;
import com.rest.comm.config.security.JwtTokenProvider;
import com.rest.comm.service.ResponseService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.*;

@Api(tags = {"1. Sign"})
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/v1")
public class SignController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JwtTokenProvider jwtTokenProvider;
    private final ResponseService responseService;
    private final PasswordEncoder passwordEncoder;

    @Resource(name="signService")
    private SignService signService;

    @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.")
    @PostMapping(value = "/signin")
    public SingleResult<String> signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id,
                                       @ApiParam(value = "비밀번호", required = true) @RequestParam String password) {

        Map<String, Object> param = new HashMap<String, Object>();
        param.put("userId", id);
        param.put("password", password);

        User user = signService.getUser(param);
        String endPass = passwordEncoder.encode(user.getPassword());

        if (!passwordEncoder.matches(password, endPass)) throw new CEmailSigninFailedException();

        return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles()));
    }

}
package com.rest.api.controller.v1;

import com.rest.api.entity.User;
import com.rest.api.model.response.CommonResult;
import com.rest.api.model.response.SingleResult;
import com.rest.api.service.SignService;
import com.rest.comm.advice.exception.CEmailSigninFailedException;
import com.rest.comm.config.security.JwtTokenProvider;
import com.rest.comm.service.ResponseService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.*;

@Api(tags = {"1. Sign"})
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/v1")
public class SignController {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private final JwtTokenProvider jwtTokenProvider;
    private final ResponseService responseService;
    private final PasswordEncoder passwordEncoder;

    @Resource(name="signService")
    private SignService signService;

    @ApiOperation(value = "로그인", notes = "이메일 회원 로그인을 한다.")
    @PostMapping(value = "/signin")
    public SingleResult<String> signin(@ApiParam(value = "회원ID : 이메일", required = true) @RequestParam String id,
                                       @ApiParam(value = "비밀번호", required = true) @RequestParam String password) {

        Map<String, Object> param = new HashMap<String, Object>();
        param.put("userId", id);
        param.put("password", password);

        User user = signService.getUser(param);
        String endPass = passwordEncoder.encode(user.getPassword());

        if (!passwordEncoder.matches(password, endPass)) throw new CEmailSigninFailedException();

        return responseService.getSingleResult(jwtTokenProvider.createToken(String.valueOf(user.getMsrl()), user.getRoles()));
    }

}
package com.rest.api.dao.mssql;

import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;

@Component
public class MssqlRestDao {

    @Autowired
    @Qualifier("mssqlSqlSessionTemplate")
    private SqlSessionTemplate sqlSession;

    /* test 조회 */
    public List<Map> selectList(Object param) {
        return sqlSession.selectList("com.tbUser.select1", param);
    }
}
package com.rest.api.dao.mssql;

import com.rest.api.entity.User;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class SignDao {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    @Qualifier("mssqlSqlSessionTemplate")
    private SqlSessionTemplate sqlSession;

    /* test 조회 */
    public User getUser(Object param) {

        User user = sqlSession.selectOne("v1.getUser", param);

        return user;
    }
}
package com.rest.api.entity;

import lombok.*;
import org.springframework.security.core.*;
import org.springframework.security.core.authority.*;
import org.springframework.security.core.userdetails.*;

import java.util.*;
import java.util.stream.*;

@ToString
public class User implements UserDetails {
    String msrl;
    String password;
    String username;

    public String getMsrl() {
        return msrl;
    }

    public String getPassword() {
        return password;
    }

    public String getUsername() {
        return username;
    }

    public void setMsrl( String msrl ) {
        this.msrl = msrl;
    }

    public void setPassword( String password ) {
        this.password = password;
    }

    public void setUsername( String username ) {
        this.username = username;
    }

    public List<String> getRoles() {
        return roles;
    }

    public void setRoles( List<String> roles ) {
        this.roles = roles;
    }

    private List<String> roles = new ArrayList<>();

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles.stream().map( SimpleGrantedAuthority :: new ).collect( Collectors.toList() );
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

package com.rest.api.service;

import com.rest.api.entity.User;

public interface SignService {
    public User getUser(Object param);
}
package com.rest.api.service;

import com.rest.api.dao.mssql.SignDao;
import com.rest.api.entity.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service("signService")
public class SignServiceImpl implements SignService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    SignDao signDao;

    @Override
    public User getUser(Object param) {
        logger.debug("[SignServiceImpl >> getUser]");
        return signDao.getUser(param);
    }
}
package com.rest.comm.config.security;

import lombok.extern.slf4j.*;
import org.springframework.security.core.*;
import org.springframework.security.core.context.*;
import org.springframework.web.filter.*;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {

    private JwtTokenProvider jwtTokenProvider;

    // Jwt Provier 주입
    public JwtAuthenticationFilter( JwtTokenProvider jwtTokenProvider ) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    /**
     * <pre>
     * Request로 들어오는 Jwt Token의 유효성을 검증(jwtTokenProvider.validateToken)하는 filter를 filterChain에 등록합니다.
     * </pre>
     *
     * @param request
     * @param response
     * @param filterChain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter( ServletRequest request, ServletResponse response, FilterChain filterChain ) throws IOException, ServletException {
        String token = jwtTokenProvider.resolveToken( (HttpServletRequest) request );

        if( token != null && jwtTokenProvider.validateToken( token ) ) {
            Authentication auth = jwtTokenProvider.getAuthentication( token );
            SecurityContextHolder.getContext().setAuthentication( auth );
        }

        filterChain.doFilter( request, response );
    }
}
package com.rest.comm.config.security;

import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;

@RequiredArgsConstructor
@Component
public class JwtTokenProvider { // JWT 토큰을 생성 및 검증 모듈

    private static Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

    @Value("${spring.jwt.secret}")
    private String secretKey;

    private long tokenValidMilisecond = 1000L * 60 * 60; // 1시간만 토큰 유효

    private final UserDetailsService userDetailsService;

    @PostConstruct
    protected void init() {
        secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
    }

    // Jwt 토큰 생성
    public String createToken(String userPk, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(userPk);
        claims.put("roles", roles);
        Date now = new Date();
        return Jwts.builder()
                .setClaims(claims) // 데이터
                .setIssuedAt(now) // 토큰 발행일자
                .setExpiration(new Date(now.getTime() + tokenValidMilisecond)) // set Expire Time
                .signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘, secret값 세팅
                .compact();
    }

    // Jwt 토큰으로 인증 정보를 조회
    public Authentication getAuthentication(String token) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
        return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
    }

    // Jwt 토큰에서 회원 구별 정보 추출
    public String getUserPk(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
    }

    /**
     * <pre>
     * Request의 Header에서 token 파싱 : "X-AUTH-TOKEN: jwt토큰"
     * </pre>
     *
     * @param req
     * @return
     */
    public String resolveToken(HttpServletRequest req) {
        return req.getHeader("X-AUTH-TOKEN");
    }

    /**
     * <per>
     * Jwt 토큰의 유효성 + 만료일자 확인
     * </per>
     *
     * @param jwtToken
     * @return
     */
    public boolean validateToken(String jwtToken) {
        try {

            logger.debug( "[JwtTokenProvider >> validateToken]" );

            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (SignatureException e) {
            logger.error("Invalid JWT signature", e);
            return false;
        } catch (MalformedJwtException e) {
            logger.error("Invalid JWT token", e);
            return false;
        } catch (ExpiredJwtException e) {
            logger.error("Expired JWT token", e);
            return false;
        } catch (UnsupportedJwtException e) {
            logger.error("Unsupported JWT token", e);
            return false;
        } catch (IllegalArgumentException e) {
            logger.error("JWT claims string is empty.", e);
            return false;
        }catch (Exception e) {
            return false;
        }
    }
}
package com.rest.comm.config.security;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@RequiredArgsConstructor
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    private final JwtTokenProvider jwtTokenProvider;

    private static final String[] SWAGGER_URI = {
            "/swagger-ui.html", "/v2/api-docs", "/swagger-resources/**", "/webjars/**", "/swagger/**"
    };

    private static final String[] PUBLIC_URI = {
            "/*/signin", "/*/signin/**", "/*/signup", "/*/signup/**"
    };

    private static final String[] PUBLIC_GET_URI = {
            "/exception/**", "/helloworld/**", "/actuator/health"
    };

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .httpBasic().disable() // rest api 이므로 기본설정 사용안함. 기본설정은 비인증시 로그인폼 화면으로 리다이렉트 된다.
                .csrf().disable() // rest api이므로 csrf 보안이 필요없으므로 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // jwt token으로 인증할것이므로 세션필요없으므로 생성안함.
                .and()
                .authorizeRequests()                                                        // 다음 리퀘스트에 대한 사용권한 체크(리소스 별 허용 범위 설정)
                .antMatchers(PUBLIC_URI).permitAll()                                            // 가입 및 인증 주소는 누구나 접근가능
                .antMatchers(HttpMethod.GET, PUBLIC_GET_URI).permitAll()                        // 등록된 GET요청 리소스는 누구나 접근가능
                //.antMatchers("/*/users").hasRole( "ADMIN" )                                   // '/users' api는 ROLE_ADMIN 만 가능함.
                .anyRequest().hasRole("USER")                                                   // 그외 나머지 요청은 모두 인증된 회원만 접근 가능
                .and()
                .exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler())            // 인증 오류 발생 시 처리를 위한 핸들러 추가
                .and()
                .exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())  // 인증 오류 발생 시 처리를 위한 핸들러 추가
                .and()
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); // jwt token 필터를 id/password 인증 필터 전에 넣어라.

    }

    @Override // ignore swagger security config
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(SWAGGER_URI);

    }
}
package com.rest.comm.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * <pre>
 *     참조 : 다중 데이터베이스 설정 https://parandol.tistory.com/17
 *     소스 : https://github.com/parandol/public-projects/blob/master/springboot.multi.database.example/src/main/java/multi/database/db1/dao/Db1Mapper.java
 * </pre>
 */
@Configuration
@PropertySource("classpath:/application.yml")
@MapperScan(value = "multi.database.mssql.dao", sqlSessionFactoryRef = "mssqlSqlSessionFactory")
public class MssqlConfig {
    private static final Logger logger = LoggerFactory.getLogger(MssqlConfig.class);

    @Autowired
    private ApplicationContext applicationContext;

    @Value("${spring.mssql.datasource.mapper-locations}")
    private String mapperLocations;

    @Value("${spring.mssql.datasource.mybatis-config}")
    private String configPath;

    @Bean(name = "mssqlDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.mssql.datasource")
    public DataSource mssqlDataSource() {

        DataSource dataSource = DataSourceBuilder.create().build();

        logger.info("[MssqlConfig >> mssqlDataSource] Datasource : {}", dataSource);

        return dataSource;
    }

    @Bean(name = "mssqlSqlSessionFactory")
    @Primary
    public SqlSessionFactory mssqlSqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(applicationContext.getResources(mapperLocations));

        //Mybatis config파일 위치
        Resource myBatisConfig = new PathMatchingResourcePatternResolver().getResource(configPath);
        sqlSessionFactoryBean.setConfigLocation(myBatisConfig);

        return sqlSessionFactoryBean.getObject();
    }

    @Bean(name = "mssqlSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate mssqlSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}
package com.rest.comm.service.security;

import com.rest.api.entity.User;
import com.rest.api.service.*;
import lombok.RequiredArgsConstructor;
import org.slf4j.*;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import javax.annotation.*;
import java.util.*;

@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(CustomUserDetailService.class);

    @Resource(name="signService")
    private SignService signService;

    public UserDetails loadUserByUsername(String userPk) {

        Map<String, Object> param = new HashMap<String, Object>();
        param.put("userId", userPk);

        User result = signService.getUser(param);
        List<String> list = new ArrayList<String>();
        list.add("ROLE_USER");
        list.add("ADMIN");
        result.setRoles(list);

        return result;
    }
}
package com.rest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.client.RestTemplate;

/**
 * <pre>
 *     출처 : https://javaengine.tistory.com/entry/SpringBoot2%EB%A1%9C-Rest-api-%EB%A7%8C%EB%93%A4%EA%B8%B08-%E2%80%93-SpringSecurity-%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EC%9D%B8%EC%A6%9D-%EB%B0%8F-%EA%B6%8C%ED%95%9C%EB%B6%80%EC%97%AC
 *     출처 : https://github.com/codej99/SpringRestApi/tree/feature/security
 * </pre>
 */

@SpringBootApplication(scanBasePackages = {"com.rest.api", "com.rest.comm"})
public class SpringRestApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringRestApiApplication.class, args);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

User_SqlMapper.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="v1">
    
    <select id="getUser" parameterType="Map" resultType="com.rest.api.entity.User">
/* UserSqlMapper - select */
SELECT #{userId}       as MSRL
     , #{password}     as PASSWORD
     , 'test@test.com' as UID
     , '홍길동'        as USERNAME
     , 'ROLE_USER'     as ROLES
    </select>

</mapper>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
		<!-- <setting name="cacheEnabled" value="true"/> default value -->
		<!-- <setting name="cacheEnabled" value="false"/> -->
		<setting name="lazyLoadingEnabled" value="false"/>
		<!-- <setting name="multipleResultSetsEnabled" value="true"/> default value -->
		<setting name="useColumnLabel" value="true"/> <!-- default value-->
		<!-- <setting name="useGeneratedKeys" value="false"/> default value -->
		<!-- <setting name="defaultExecutorType" value="SIMPLE"/> default value -->
		<setting name="defaultStatementTimeout" value="25000"/>
		<setting name="mapUnderscoreToCamelCase" value="true"/>
		<setting name="localCacheScope" value="STATEMENT"/>
		<setting name="jdbcTypeForNull" value="CHAR"/>
	</settings>

</configuration>

application.yml

spring:
  profiles:
    active: local                       # 디폴트 환경
  jwt:
    secret: govlepel@$&
  messages:
    basename: i18n/exception
    encoding: UTF-8
  # DB 설정
  mssql:
    datasource:
      driver-class-name: net.sf.log4jdbc.sql.jdbcapi.DriverSpy
      jdbc-url: jdbc:log4jdbc:sqlserver://localhost:1433;databaseName=SampleRest
      username: SAMPLE
      password: SAMPLE
      auto-commit: false
      connection-test-query: SELECT 1
      minimum-idle: 3
      maximum-pool-size: 10
      pool-name: pool-mssql
      # mapper.xml 위치 지정
      # **은 하위 폴더 레벨에 상관없이 모든 경로를 뜻하며, *는 아무 이름이나 와도 된다는것을 뜻합니다.
      mapper-locations: classpath:/mybatis/mapper/mssql/**/*.xml
      # mapper.xml에서 resultType을 지정할 때 com.god.bo.test.vo.TestVo 대신 TestVo로 간략히 할 수 있다.
      #type-aliases-package: com.god.bo.test.vo
      mybatis-config: classpath:/mybatis/mybatis-config.xml
mybatis:
  configuration:
    map-underscore-to-camel-case: true

application-local.yml

server:
  port: 8090                                # 서버 포트 변경

logging:
  level:
    root: warn

spring:
  profiles: local

log4jdbc.log4j2.properties

log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
log4jdbc.dump.sql.maxlinelength=0

#Disable - Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
log4jdbc.auto.load.popular.drivers = false

log4j2-spring.yml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout
                    pattern="%style{%d{ISO8601}}{black} %highlight{%-5level }[%style{%t}{bright,blue}] %style{%C{1.}}{bright,yellow}: %msg%n%throwable" />
        </Console>

        <RollingFile name="RollingFile"
                     fileName="./logs/spring-boot-sample.log"
                     filePattern="./logs/spring-boot-sample-%d{yyyy-MM-dd}-%i.log.gz">
            <PatternLayout>
                <pattern>%d %p %C{1.} [%t] %m%n</pattern>
            </PatternLayout>
            <Policies>
                <!-- rollover on startup, daily and when the file reaches
                    10 MegaBytes -->
                <OnStartupTriggeringPolicy />
                <SizeBasedTriggeringPolicy
                        size="10 MB" />
                <TimeBasedTriggeringPolicy />
            </Policies>
        </RollingFile>
    </Appenders>

    <Loggers>

        <Logger name="jdbc.sqltiming" level="INFO" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Logger>
        <Logger name="jdbc.sqlonly" level="INFO" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Logger>

        <!-- log4jdbc logging -->
        <Logger name="jdbc" level="error" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Logger>
        <Logger name="log4jdbc.log4j2" level="error" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Logger>

        <Logger name="com.rest" level="debug" additivity="false">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Logger>

        <!-- LOG everything at INFO level -->
        <Root level="info">
            <AppenderRef ref="console" />
            <AppenderRef ref="RollingFile" />
        </Root>
    </Loggers>

</Configuration>

테스트

728x90