728x90

SpringBoot version:2.1.6

SpringSecurity version: 5.1.5

MyBatis-Plus version: 3.1.0

JDK version:1.8 (17에서도 잘 돌아감)

 

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yml

# 포트
server:
  port: 8080
spring:
  # datasource 설정
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/demo_security?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
# jwt 설정
jwt:
  secret: JWTSecret
  tokenHeader: Authorization
  tokenPrefix: Sans-

  expiration: 86400 # 1day ,604800 1 weekly
  # 인증필요없는 url
  antMatchers: /index/**,/login/**,/favicon.ico
# Mybatis-plus 관련 설정
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    db-config:
      id-type: AUTO
      field-strategy: NOT_EMPTY
      db-type: MYSQL
  configuration:
    map-underscore-to-camel-case: true
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

기본적인 Entity,Dao,Service 와 SpringSecurity user Entity,Service 부분은 여기서 생략한다.

Jwt에 사용된 util 작성

@Slf4j
public class JWTTokenUtil {

    private JWTTokenUtil(){}

    public static String createAccessToken(SelfUserEntity selfUserEntity){
        // jwt 생성 부분
        String token = Jwts.builder()
                .setId(selfUserEntity.getUserId()+"")
                .setSubject(selfUserEntity.getUsername())
                // 발급시간
                .setIssuedAt(new Date())
                // 발급자 임의로 수정가능
                .setIssuer("demo-user")
                // 권한설정
                .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities()))
                // 만료시간
                .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration))
                // 싸인알고리즘, yml에 기재된 시큐릿 key값
                .signWith(SignatureAlgorithm.HS512, JWTConfig.secret)
                .compact();
        return token;
    }
}

권한없을시 처리될 부분을 작성한다.

@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler{

    /**
     * 권한없을시 반환하는 부분
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"권한없음!"));
    }
}

사용자 미등록시 처리되는 부분

@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {

    /**
     * 로그인하지 않는 사용자일때 반환하는 부분
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"로그인되지않음"));
    }
}

로그인실패시 처리될 부분작성

@Slf4j
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {

    /**
     * 로그인 실패시 리턴
     * @param request
     * @param response
     * @param exception
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){

        if (exception instanceof UsernameNotFoundException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"존재하지 않는 사용자!"));
        }
        if (exception instanceof LockedException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"동결된 사용자!"));
        }
        if (exception instanceof BadCredentialsException){
            log.info("[로그인실패!]"+exception.getMessage());
            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"패스워드가 틀림."));
        }
        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"로그인 실패!"));
    }
}

로그인 성공시 처리될 부분작성

@Slf4j
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();
        String token = JWTTokenUtil.createAccessToken(selfUserEntity);
        token = JWTConfig.tokenPrefix + token;

        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "로그인성공!");
        resultData.put("token",token);
        ResultUtil.responseJson(response,resultData);
    }
}

로그아웃처리 부분

@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        Map<String,Object> resultData = new HashMap<>();
        resultData.put("code","200");
        resultData.put("msg", "로그인성공!");
        SecurityContextHolder.clearContext();
        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
    }
}

로그인 인증서 핵심로직부분이다.

@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private SelfUserDetailsService selfUserDetailsService;
    @Autowired
    private SysUserService sysUserService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String userName = (String) authentication.getPrincipal();
        String password = (String) authentication.getCredentials();
        SelfUserEntity userInfo = selfUserDetailsService.loadUserByUsername(userName);
        if (userInfo == null) {
            throw new UsernameNotFoundException("사용자존재하지않음");
        }
        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
            throw new BadCredentialsException("패스워드틀림");
        }
        if (userInfo.getStatus().equals("PROHIBIT")){
            throw new LockedException("사용자가동결됨 ");
        }
        Set<GrantedAuthority> authorities = new HashSet<>();
        List<SysRoleEntity> sysRoleEntityList = sysUserService.selectSysRoleByUserId(userInfo.getUserId());
        for (SysRoleEntity sysRoleEntity: sysRoleEntityList){
            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));
        }
        userInfo.setAuthorities(authorities);
        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
    }
    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}

PermissionEvaluator 컨트롤러에 어노테이션으로 체크하는 인증부분 구현 하는 부분이다.

@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
    @Autowired
    private SysUserService sysUserService;


    @Override
    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();
        Set<String> permissions = new HashSet<>();
        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());
        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {
            permissions.add(sysMenuEntity.getPermission());
        }
        if (permissions.contains(permission.toString())){
            return true;
        }
        return false;
    }
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
        return false;
    }
}

SpringSecurity 핵심 비지니스로직 구현 부분이다.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 요거 추가하면 controller 쪽에 어노테이션으로 권한 처리 할수 있음.
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserLoginSuccessHandler userLoginSuccessHandler;

    @Autowired
    private UserLoginFailureHandler userLoginFailureHandler;

    @Autowired
    private UserLogoutSuccessHandler userLogoutSuccessHandler;

    @Autowired
    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;

    @Autowired
    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;

    @Autowired
    private UserAuthenticationProvider userAuthenticationProvider;


    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(new UserPermissionEvaluator());
        return handler;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth){
        auth.authenticationProvider(userAuthenticationProvider);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
               .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
                .anyRequest().authenticated()
                .and()

                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
                .and()
                .formLogin()
                .loginProcessingUrl("/login/userLogin")
                .successHandler(userLoginSuccessHandler)
                .failureHandler(userLoginFailureHandler)
                .and()

                .logout()
                .logoutUrl("/login/userLogout")
                .logoutSuccessHandler(userLogoutSuccessHandler)
                .and()
                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
                .and()
                .cors()
                .and()
                .csrf().disable();

        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.headers().cacheControl();
        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
    }
}

JWT 필터도 만들어봐야지

@Slf4j
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // token 헤더정보 가져오기
        String tokenHeader = request.getHeader(JWTConfig.tokenHeader);
        if (null!=tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
            try {
                String token = tokenHeader.replace(JWTConfig.tokenPrefix, "");
                // jwt 파싱
                Claims claims = Jwts.parser()
                        .setSigningKey(JWTConfig.secret)
                        .parseClaimsJws(token)
                        .getBody();
                // user정보
                String username = claims.getSubject();
                String userId=claims.getId();
                if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) {
                    // role 정보
                    List<GrantedAuthority> authorities = new ArrayList<>();
                    String authority = claims.get("authorities").toString();
                    if(!StringUtils.isEmpty(authority)){
                        List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class);
                        for(Map<String,String> role : authorityMap){
                            if(!StringUtils.isEmpty(role)) {
                                authorities.add(new SimpleGrantedAuthority(role.get("authority")));
                            }
                        }
                    }
                    // 파라미터 셋팅
                    SelfUserEntity selfUserEntity = new SelfUserEntity();
                    selfUserEntity.setUsername(claims.getSubject());
                    selfUserEntity.setUserId(Long.parseLong(claims.getId()));
                    selfUserEntity.setAuthorities(authorities);
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            } catch (ExpiredJwtException e){
                log.info("Token 유효기간 만료");
            } catch (Exception e) {
                log.info("Token 유효하지 않는 토큰");
            }
        }
        filterChain.doFilter(request, response);
    }
}

테스트 코드도 작성 , (회원가입이 잘되는지... 물론 회원가입 controller는 따론 구현하지 않아서 일단 이런 식으로 강제로 user를 생성해 본다.)

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootSecurityDemoApplicationTests {


    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    @Autowired
    private SysUserRoleService sysUserRoleService;



    @Test
    public void contextLoads() {
        // 가입
        SysUserEntity sysUserEntity = new SysUserEntity();
        sysUserEntity.setUsername("user");
        sysUserEntity.setPassword(bCryptPasswordEncoder.encode("123456"));
        // 사용자 status
        sysUserEntity.setStatus("NORMAL");
        sysUserService.save(sysUserEntity);
        // role setting 1:ADMIN 2:USER
        SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();
        sysUserRoleEntity.setRoleId(2L);
        sysUserRoleEntity.setUserId(sysUserEntity.getUserId());
        sysUserRoleService.save(sysUserRoleEntity);
    }

}

postman 으로 조금전에 생성한 사용자로 로그인 해보자

짜쟌~~

user 정보 조회도 해보자

admin 으로 접근을 하니 권한이 없다고 나온다. 물론 admin 권한은 모두 볼수 있어야 되는거 아니야 ? 라고 할수 있는데 해당 demo에는 그런거 따로 설정을 안해서 이다.

테스트 코드로 관리자 admin 이라는 사용자 생성 , role : ADMIN 으로 설정하고 다시 로그인 하여 새로은 admin jwt 을 받고 

/admin/info api를 호출하면 권한이 정상으로 인증되서 정보를 정상조회할수 있다.

일단 잘되니깐 나머지 controller도 만들어서 테스트하면 될것다.

 

인터넷에 찾아보면 너무 많은 방대한 가이드들이 있긴 하지만 한번 이대로 따라 해보는것도 좋을것다.

그리고 Spring Security 5.7.0 이전/이후 버전으로 설정이 조금 다르다.

해당글은 Spring Security 5.7.0 이전 버전으로 진행한것이다.

추후 업그레이드된 Spring Security 5.7.0 이후 버전 설정 방법으로 글을 쓰도록 하겠다.

 

아참 혹시나 필요할것 같아서 소스 코드도 깃에 업로드 합니다.

필요한 분은 클론 받으시고 비록 정답은 아니지만 프로젝트 적용에 조금이나 도움되엇으면 합니다.

https://github.com/luxury515/spring-boot-security-demo

끝!

 

참고문서

https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

https://github.com/spring-projects/spring-security/issues/10822

+ Recent posts