[Spring Security] Back-End - Spring Security설정(With JWT) - 2

2024. 11. 28. 13:31JAVA/Spring Security

환경설정이 끝났으니 본격적으로 Security 설정을 시작하도록 한다.

1. Spring Security Config 

Security 및 Annotation에 대해서는 별도 설명을 하지 않는다.

package kr.co.infob.config.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

	@Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

		http
			.csrf(AbstractHttpConfigurer::disable)

			//Login, Logout 등 기본 방식 미사용 처리
			.formLogin(AbstractHttpConfigurer::disable)
			.logout(AbstractHttpConfigurer::disable)
			.httpBasic(AbstractHttpConfigurer::disable)

			//JWT를 사용할 예정으로 Session 사용하지 않음
			.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

			//요청 URL 검증 추리
			.authorizeHttpRequests(requests ->
							requests
								.anyRequest()
									.permitAll()
						)
		;

		return http.build();
	}

}

우선 기본 설정만한 상태이다.
JWT를 사용하기 때문에 로그인, 로그아웃 관련해서는 사용하지 않는 것으로 하였고
또한 JWT 사용으로 Session 사용을 하지 않는 것으로 설정하였다.

테스트를 위해 모든 요청에 대해서 권한 체크를 하지 않도록 설정하였다.

테스트 Controller를 생성한다.

package kr.co.infob.api.auth.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {

	@GetMapping("/signup")
	public ResponseEntity<?> signup() {
		return ResponseEntity.ok("Sign UP!!!!!!");
	}

}

위와 같이 Controller을 생성하고 테스트를 진행한다.
테스트는 Postman을 사용하여 진행한다.

위와 같이 찍히면 우선 1차적으로 성공했다.

이제부터 Security를 하나씩 체워 나가기로 한다.


2. Entity 및 Vo생성

나의 경우에는 사용자 정보 및 권한 정보 등을 관리하는 entityVo를 생성하여 사용한다.

UserDetailsVo : UserDetails Interface를 구현한 Class 사용자에 대한 정보를 담는 Class
Grant : GrantedAuthority를 구현한 Class로 권한 정보를 담는 
UrlRoleMapping : Database에서 URL에 따른 권한을 조회하기 위한 Class

common.database.entity 안에 있는 class는 Database 테이블과 1:1 맵핑되는 Class로 구성하였다.
(이건 내가 쓰는 개취...)

RoleInfo.java

package kr.co.infob.common.database.entity;

import java.util.Date;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class RoleInfo {

	private String roleCd;

	private String roleNm;

	private String useYn;

	private String registerId;

	private Date registDttm;

	private String updusrId;

	private Date updtDttm;

}


UserInfo.java

package kr.co.infob.common.database.entity;

import java.util.Date;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.SuperBuilder;

@Getter @Setter
@SuperBuilder
@NoArgsConstructor
public class UserInfo {

	private String userId;

	private String passwd;

	private String userNm;

	private String cttpc;

	private String email;

	private Date registDttm;

	private String updusrId;

	private Date updtDttm;

}

Grant.java

package kr.co.infob.config.security.vo;

import org.springframework.security.core.GrantedAuthority;

import kr.co.infob.common.database.entity.RoleInfo;

@SuppressWarnings("serial")
public class Grant extends RoleInfo implements GrantedAuthority {

	@Override
	public String getAuthority() {
		return getRoleCd();
	}

}

UrlRoleMapping.java

package kr.co.infob.config.security.vo;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class UrlRoleMapping {

	private String resrcUrl;

	private String roleCd;

}

UserDetailsVo.java

package kr.co.infob.config.security.vo;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import kr.co.infob.common.database.entity.UserInfo;
import lombok.Getter;
import lombok.experimental.SuperBuilder;

@SuppressWarnings("serial")
@Getter
@SuperBuilder
public class UserDetailsVo extends UserInfo implements UserDetails {

	private Set<GrantedAuthority> authorities;

	private List<Grant> roles;


	@Override
	public String getUsername() {
		return getUserId();
	}

	@Override
	public String getPassword() {
		return getPasswd();
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {

		if(authorities == null){
			authorities = new HashSet<GrantedAuthority>();
			for(Grant grant : getRoles()){
				authorities.add(grant);
			}
		}
		return authorities;
	}

	/**
     * 계정이 만료되지 않았는지 여부를 반환합니다.
     * 현재 항상 false를 반환하므로, 모든 계정이 만료된 것으로 처리됩니다.
     *
     * @return boolean
     */
	@Override
	public boolean isAccountNonExpired() {
		return false;
	}

	/**
     * 계정이 잠기지 않았는지 여부를 반환합니다.
     *
     * @return boolean
     */
	@Override
    public boolean isAccountNonLocked() {
        return false;
    }

	/**
     * 자격 증명(비밀번호)이 만료되지 않았는지 여부를 반환합니다.
     *
     * @return boolean
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    /**
     * 계정이 활성화되어 있는지 여부를 반환합니다.
     *
     * @return boolean
     */
    @Override
    public boolean isEnabled() {
        return false;
    }

}

 

3. SecurityService 

Security에서 사용할 사용자 정보 및 Database에서 URL과 권한을 조회 하도록 한다.

package kr.co.infob.config.security.service;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import kr.co.infob.config.security.SecurityMapper;
import kr.co.infob.config.security.vo.UrlRoleMapping;
import kr.co.infob.config.security.vo.UserDetailsVo;

@Service
public class SecurityService {

	@Autowired
	private SecurityMapper securityMapper;

	/**
	 * 사용자 정보를 조회 하여 UserDetails에 담는다.
	 * @param username
	 * @return
	 */
	public UserDetailsVo loadUserByUsername(String username) {


		UserDetailsVo user = securityMapper.getUsername(username);

		if( ObjectUtils.isEmpty(user)) {
			throw new UsernameNotFoundException("User not found.");
		}

		Set<GrantedAuthority> authorities = user.getRoles()
                                    .stream()
                                        .map((role) -> new SimpleGrantedAuthority(role.getRoleCd()))
                                        .collect(Collectors.toSet());

		return UserDetailsVo.builder()
				.userId(user.getUsername())
				.passwd(user.getPasswd())
				.authorities(authorities)
				.build();
	}

	/**
	 * Database에서 URL에 따른 권한 정보를 호출하여 접근 여부를 확인하는데 사용한다.
	 * @return
	 */
	public LinkedHashMap<RequestMatcher, List<ConfigAttribute>> selectUrlRoleMapping() {

		LinkedHashMap<RequestMatcher, List<ConfigAttribute>> resourcesMap = new LinkedHashMap<RequestMatcher, List<ConfigAttribute>>();

		List<UrlRoleMapping> resultList = securityMapper.selectUrlRoleMapping();
		String preResource = null;
		RequestMatcher presentResource;
		List<ConfigAttribute> configList;

		for(UrlRoleMapping vo : resultList){
			presentResource = new AntPathRequestMatcher(vo.getResrcUrl());

			if(preResource != null && vo.getResrcUrl().equals(preResource)){
				List<ConfigAttribute> preAuthList = resourcesMap.get(presentResource);
				preAuthList.add(new SecurityConfig(vo.getRoleCd()));
			}else{
				configList = new LinkedList<ConfigAttribute>();
				configList.add(new SecurityConfig(vo.getRoleCd()));
				resourcesMap.put(presentResource, configList);
			}

			preResource = vo.getResrcUrl();
		}

		return resourcesMap;
	}

}

 

4. SecurityMapper 설정

SecurityMapper.java

package kr.co.infob.config.security;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

import kr.co.infob.config.security.vo.UrlRoleMapping;
import kr.co.infob.config.security.vo.UserDetailsVo;

@Mapper
public interface SecurityMapper {

	/**
	 * 사용자 정보 조회
	 * @param username
	 * @return
	 */
	UserDetailsVo getUsername(String username);

	/**
	 * URL에 따른 롤정보 조회
	 * @return
	 */
	List<UrlRoleMapping> selectUrlRoleMapping();

}

security.xml

Mybatis Query 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="kr.co.infob.config.security.mapper.SecurityMapper">

	<resultMap type="kr.co.infob.config.security.vo.Grant" id="resultGrant">
		<result property="roleCd" column="ROLE_CD"/>
		<result property="roleNm" column="ROLE_NM"/>
	</resultMap>

	<resultMap type="kr.co.infob.config.security.vo.UserDetailsVo" id="resultUserDetailsVo" extends="kr.co.infob.common.database.mapper.UserInfoMapper.resultUserInfo">
		<collection property="roles" resultMap="resultGrant" />
	</resultMap>

	<select id="getUsername" resultMap="resultUserDetailsVo">
		/* kr.co.infob.config.security.mapper.SecurityMapper.getUsername */
		SELECT
			UI.USER_ID,
			UI.USER_NM,
			UI.CTTPC,
			UI.EMAIL,
			RI.ROLE_CD,
			RI.ROLE_NM
		  FROM USER_INFO UI
			LEFT JOIN USER_ROLE UR ON UI.USER_ID = UR.USER_ID
			LEFT JOIN ROLE_INFO RI ON UR.ROLE_CD = RI.ROLE_CD
		WHERE
			UI.USER_ID = #{username}
	</select>


	<resultMap type="kr.co.infob.config.security.vo.UrlRoleMapping" id="resultUrlRoleMapping">
		<result property="resrcUrl" column="RESRC_URL"></result>
		<result property="roleCd" column="ROLE_CD"></result>
	</resultMap>

	<select id="selectUrlRoleMapping" resultMap="resultUrlRoleMapping">
		/* kr.co.infob.config.security.mapper.SecurityMapper.getUsername */
		SELECT RESRC.RESRC_URL, ROLE.ROLE_CD, RESRC.RESRC_ORD, 0 ACCESS_RESRC_ORD
		  FROM RESRC_INFO RESRC
			  CROSS JOIN ROLE_INFO ROLE
		WHERE ACCESS_TYPE_CD = 'COMM'
		  AND RESRC.RESRC_USE_YN = 'Y'
		UNION ALL
		SELECT RESRC.RESRC_URL, ROLE.ROLE_CD, RESRC.RESRC_ORD, 0 ACCESS_RESRC_ORD
		  FROM RESRC_INFO RESRC
			  CROSS JOIN (
				SELECT ROLE_CD FROM ROLE_INFO
				UNION ALL
				SELECT 'ANONYMOUS' ROLE_CD
			  ) ROLE
		WHERE ACCESS_TYPE_CD = 'ALL'
		  AND RESRC.RESRC_USE_YN = 'Y'
		UNION ALL
		SELECT ACCESS_RESRC_PTTRN RESRC_URL, ROLE_CD, 1 RESRC_ORD,  ACCESS_RESRC_ORD
		  FROM (
			  SELECT MI.MENU_SN, ROLE_CD, MI.RESRC_SN
			  FROM MENU_INFO MI
				INNER JOIN MENU_ROLE MR ON MI.MENU_SN = MR.MENU_SN AND MR.READ_AUTH_YN = 'Y'
			) MI
			INNER JOIN RESRC_ACCESS_AUTH RAA ON MI.RESRC_SN = RAA.RESRC_SN AND RAA.ACCESS_AUTH_SE_CD = 'READ'
		UNION ALL
		SELECT ACCESS_RESRC_PTTRN RESRC_URL, ROLE_CD, 1 RESRC_ORD, ACCESS_RESRC_ORD
		  FROM (
			  SELECT MI.MENU_SN, MR.ROLE_CD, MI.RESRC_SN
			  FROM MENU_INFO MI
				INNER JOIN MENU_ROLE MR ON MI.MENU_SN = MR.MENU_SN AND MR.WRITE_AUTH_YN = 'Y'
			) MI
			INNER JOIN RESRC_ACCESS_AUTH RAA ON MI.RESRC_SN = RAA.RESRC_SN AND RAA.ACCESS_AUTH_SE_CD = 'WRITE'
		UNION ALL
		SELECT '/**' RESRC_URL, 'SYSADM' ROLE_CD, 99 RESRC_ORD, 99 ACCESS_RESRC_ORD
		ORDER BY RESRC_ORD,  ACCESS_RESRC_ORD, RESRC_URL
	</select>

</mapper>

참고로 여기서 사용하는 권한은 SYSADM(시스템관리자), USER(사용자)로 한다.

getUsername
별도의 설명은 하지 않겠다.

selectUrlRoleMapping 

리스스 정보를 기본으로 리스스에 접근 권한이 있는지와 메뉴에 대한 권한이 있는지 여부등을 기준으로 
전체 리소스의 정보를 조회한다.

리소스 정보를 우선 이렇게 입력을 했다.
위와 같이 정보를 입력하고 조회해 보면 바로 이해 될거라 본다.

내용이 길어져서 여기서 끝.