일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바스크립트 이벤트 루프
- HTTP Web Server
- Navigation Pattern
- Linux 파일 관리 명령어
- javascript scope
- 서버의 서비스 방식
- linux foreground
- EC2 Apache2
- linux background
- EC2 oh my zsh
- Linux 디렉터리 역할
- Linux pwd
- JavaScript 실행 디버깅
- Linux 디렉터리 구조
- Linux cd
- Logback
- JavaScript EventLoop
- Linux ls
- Linux mkdir
- 자바스크립트 런타임
- EC2 HTTP 호스팅
- AWS EC2 서버 만들기
- EC2 zsh
- Linux apt
- Linux 디렉터리 명령어
- Linux cat
- Linux rmdir
- javascript 정렬
- Linux oh my zsh
- Linux apt-get
Archives
- Today
- Total
HyunJun 기술 블로그
스프링 시큐리티 JWT 토큰 인증 구현 본문
728x90
반응형
1. 스프링 시큐리티란?
Spring 기반의 애플리케이션의 보안(인증, 인가)을 담당하는 스프링 하위 프레임워크이다.
- 보안 관점에서의 인증과, 권한에 따른 인가 설정을 Filter로 처리한다.
- Filter는 Dispatcher Servlet으로 가기 전에 적용된다.
- 이 글에서는 기본적인 시큐리티, JWT Access Token 활용법만 기술합니다.
2. 구현하기
- 타임 리프 + 스프링 시큐리티(JWT)로 회원가입 페이지 -> 로그인 페이지 -> 메인 페이지 -> 권한 필요 페이지 4가지를 표현하도록 구현해 볼게요.
- 권한은 멤버, 프라이빗 멤버, VIP 멤버, 관리자로 4가지 권한을 나누어 보았습니다.
- 프런트엔드 서버가 따로 있는 게 아닌 스프링 하나로 서버사이드 렌더링으로 HTML을 표현하고 있기 때문에, 스프링 서버 url로 접근할 때 토큰의 유무를 확인하기 위해서 클라이언트단에 쿠키로 저장하여 구현해 보겠습니다.
- 클라이언트는 매 요청마다 자동으로 쿠키를 서버로 전달합니다.
build.gradle
dependencies {
/*Web*/
implementation 'org.springframework.boot:spring-boot-starter-web'
/*Security*/
implementation 'org.springframework.boot:spring-boot-starter-security'
/*Thymeleaf*/
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
/*lombok*/
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
/*test*/
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
/*JPA, Mysql*/
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
/*JWT 의존성*/
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
application.yml
spring:
profiles:
include: datasource, jwt
jpa:
hibernate:
ddl-auto: update
- datasource, jwt는 공유되지 않게 따로 yml로 관리합니다.
- ddl-auto: update는 실제 사용 프로젝트가 아니므로 update로 간단하게 테이블을 생성합니다.
- datasource 정보는 노출 시 database의 data 유출 위험, 과부하 사용량 위험 등이 있으며,
jwt secret-key는 유출이 되면 토큰 생성 및 변조가 가능하므로 유출이 되면 안 됩니다!
application-datasource.yml
spring:
datasource:
url: jdbc:mysql://my---server.ap-northeast-2.rds.amazonaws.com:3306/spring-jwt
username: 유저네임
password: 비밀번호
application-jwt.yml
jwt:
secret:
key: qwrmlasfmlqw2491u4291urjeiqowfjkwklfnlksdnfi13fnou3nounf13
.gitignore
application-datasource.yml
application-jwt.yml
깃허브에 올라가지 않게 .gitignore에 등록을 합니다.
SpringSecurityConfig
package com.example.springsecurityjwt.config;
import com.example.springsecurityjwt.jwt.JwtAuthFilter;
import com.example.springsecurityjwt.jwt.JwtUtil;
import com.example.springsecurityjwt.security.UserRoleEnum;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SpringSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// resources 자원 접근 허용
return (web) -> web.ignoring()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
/* csrf 설정 해제. */
http.csrf().disable();
/*JWT 사용 위해 기존의 세션 방식 인증 해제*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
/* URL Mapping */
http.authorizeRequests()
.antMatchers(HttpMethod.GET, "/signup").permitAll()/*회원가입 페이지는 무조건 허용*/
.antMatchers(HttpMethod.GET, "/login").permitAll()/*로그인 페이지 허용.*/
.antMatchers(HttpMethod.POST, "/api/signup").permitAll()/*회원가입 요청은 무조건 허용*/
.antMatchers(HttpMethod.POST, "/api/login").permitAll()/*로그인 요청 허용*/
.antMatchers("/cookie/test").permitAll()
.antMatchers("/vip").hasRole(UserRoleEnum.VIP_MEMBER.toString())/*vip 멤버 전용 페이지*/
.antMatchers("/admin").hasRole(UserRoleEnum.ADMIN.toString())/*관리자 전용 페이지*/
.anyRequest().authenticated()/*나머지 요청은 전부 인증되어야 함*/
/*JwtAuthFilter에 jwtUtil을 전달하여 UsernamePasswordAuthenticationFilter전에 필터로 등록한다.*/
.and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
/*로그인 페이지를 /login으로 설정한다.
1. 인증이 되지 않은 사용자가 permitAll()페이지가 아닌 페이지에 접근할 때 /login으로 강제 이동 시킨다.
2. 이때의 인증은 위에 필터에 등록해 놓은 JWT 토큰의 유무(유효성 검증) 기준이다.*/
http.formLogin().loginPage("/login");
/*인가 (권한 인증) 실패 시 아래의 핸들러 작동 ex) 멤버인데 -> VIP 멤버의 페이지를 접근하는 경우*/
http.exceptionHandling().accessDeniedPage("/forbidden");
return http.build();
}
}
MemberController
package com.example.springsecurityjwt.controller;
import com.example.springsecurityjwt.dto.LoginRequestDto;
import com.example.springsecurityjwt.dto.SignUpRequestDto;
import com.example.springsecurityjwt.jwt.JwtUtil;
import com.example.springsecurityjwt.security.UserDetailsImpl;
import com.example.springsecurityjwt.service.MemberService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Controller
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
/*index 뷰*/
@GetMapping("/index")
public String index() {
return "index";
}
/*로그인 뷰*/
@GetMapping("/login")
public String login(@AuthenticationPrincipal UserDetailsImpl userDetails) {
/*이미 로그인된 사용자일 경우 인덱스 페이지로 강제이동.*/
if (userDetails != null) {
log.info(userDetails.getMember().getMemberName() + "님이 로그인 페이지로 이동을 시도함. -> index 페이지로 강제 이동 함.");
return "redirect:/index";
}
return "login";
}
/*회원가입 뷰*/
@GetMapping("/signup")
public String signUp() {
return "signUp";
}
/*vip 전용 페이지 뷰*/
@GetMapping("/vip")
public String vip(HttpServletRequest request) {
return "role";
}
/*관리자 페이지 뷰*/
@GetMapping("/admin")
public String system(HttpServletRequest request) {
return "role";
}
/*회원가입 API*/
@PostMapping("/api/signup")
public String signUp(SignUpRequestDto requestDto) {
memberService.signUp(requestDto);
return "redirect:/login";
}
/*로그인 API*/
@PostMapping("/api/login")
public String login(LoginRequestDto requestDto, HttpServletResponse response) {
memberService.login(requestDto, response);
return "redirect:/index";
}
/*서버 로그 쿠키 테스트 확인용*/
@GetMapping("/cookie/test")
public String test(@CookieValue(value = "Authorization", defaultValue = "", required = false) String test) {
log.info(test);
return "index";
}
/*403, forbidden -> 인가 실패 시*/
@GetMapping("/forbidden")
public String forbidden() {
log.warn("비정상적인 접근: 403 forbidden");
/*따로 횟수를 기록한다던지 로직*/
return "redirect:/index";
}
/*로그아웃 API*/
@GetMapping("/api/logout")
public String logout(@CookieValue(value = "Authorization", defaultValue = "", required = false) Cookie jwtCookie,
HttpServletResponse response) {
/*jwt 쿠키를 가지고와서 제거한다.*/
jwtCookie.setValue(null);
jwtCookie.setMaxAge(0);
jwtCookie.setPath("/");
response.addCookie(jwtCookie);
return "redirect:/login";
}
}
LoginRequestDto
package com.example.springsecurityjwt.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginRequestDto {
private String memberName;
private String password;
}
SignUpRequestDto
package com.example.springsecurityjwt.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class SignUpRequestDto {
private String memberName;
private String password;
private String role;
}
Member
package com.example.springsecurityjwt.entity;
import com.example.springsecurityjwt.security.UserRoleEnum;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String memberName;
@Column(nullable = false)
private String password;
@Column(nullable = false)
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
public Member(String memberName, String password, UserRoleEnum role) {
this.memberName = memberName;
this.password = password;
this.role = role;
}
}
MemberRepository
package com.example.springsecurityjwt.repositroy;
import com.example.springsecurityjwt.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByMemberName(String memberName);
}
UserDetailsImpl
package com.example.springsecurityjwt.security;
import com.example.springsecurityjwt.entity.Member;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
@Slf4j
public class UserDetailsImpl implements UserDetails {
private final Member member;
private final String password;
private final String username;
public UserDetailsImpl(Member member, String password, String username) {
this.member = member;
this.password = password;
this.username = username;
}
public Member getMember(){
return this.member;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = member.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}/*false: 사용자 계정의 유효 기간 만료*/
@Override
public boolean isAccountNonLocked() {
return true;
}/*false: 계정 잠금 상태*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}/*false: 비밀번호 만료*/
@Override
public boolean isEnabled() {
return true;
} /*false: 유효하지 않은 사용자*/
}
UserDetailsServiceImpl
package com.example.springsecurityjwt.security;
import com.example.springsecurityjwt.entity.Member;
import com.example.springsecurityjwt.repositroy.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String memberName) throws UsernameNotFoundException {
Member member = memberRepository.findByMemberName(memberName)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
return new UserDetailsImpl(member, member.getPassword(), member.getMemberName());
}
}
UserRoleEnum
package com.example.springsecurityjwt.security;
public enum UserRoleEnum {
MEMBER(Authority.MEMBER),
PRIVATE_MEMBER(Authority.PRIVATE_MEMBER),
VIP_MEMBER(Authority.VIP_MEMBER),
ADMIN(Authority.ADMIN);
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String MEMBER = "ROLE_MEMBER";
public static final String PRIVATE_MEMBER = "ROLE_PRIVATE_MEMBER";
public static final String VIP_MEMBER = "ROLE_VIP_MEMBER";
public static final String ADMIN = "ROLE_ADMIN";
}
public static UserRoleEnum fromString(String role) {
switch (role) {
case "ROLE_MEMBER":
return MEMBER;
case "ROLE_PRIVATE_MEMBER":
return PRIVATE_MEMBER;
case "ROLE_VIP_MEMBER":
return VIP_MEMBER;
case "ROLE_ADMIN":
return ADMIN;
}
return null;
}
}
MemberService
package com.example.springsecurityjwt.service;
import com.example.springsecurityjwt.security.UserRoleEnum;
import com.example.springsecurityjwt.dto.LoginRequestDto;
import com.example.springsecurityjwt.dto.SignUpRequestDto;
import com.example.springsecurityjwt.entity.Member;
import com.example.springsecurityjwt.jwt.JwtUtil;
import com.example.springsecurityjwt.repositroy.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;
@Service
@Slf4j
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
private final JwtUtil jwtUtil;
private final PasswordEncoder passwordEncoder;
/*회원 가입*/
@Transactional
public void signUp(SignUpRequestDto requestDto) {
/*아이디*/
String memberName = requestDto.getMemberName();
/*패스워드*/
String password = passwordEncoder.encode(requestDto.getPassword());
/*유저 권한*/
UserRoleEnum role = UserRoleEnum.valueOf(requestDto.getRole());
Member member = new Member(memberName, password, role);
memberRepository.save(member);
}
/*로그인*/
@Transactional
public void login(LoginRequestDto requestDto, HttpServletResponse response) {
Optional<Member> optionalMember = memberRepository.findByMemberName(requestDto.getMemberName());
if (optionalMember.isEmpty()) {
log.warn("회원이 존재하지 않음");
throw new IllegalArgumentException("회원이 존재하지 않음");
}
Member member = optionalMember.get();
/*비밀번호 다름.*/
if (!passwordEncoder.matches(requestDto.getPassword(), member.getPassword())) {
log.warn("비밀번호가 일치하지 않습니다.");
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
/*토큰을 쿠키로 발급 및 응답에 추가*/
Cookie cookie = new Cookie(JwtUtil.AUTHORIZATION_HEADER,
jwtUtil.createToken(member.getMemberName(), member.getRole()));
cookie.setMaxAge(7 * 24 * 60 * 60); // 7일 동안 유효
cookie.setPath("/");
cookie.setDomain("localhost");
cookie.setSecure(false);
response.addCookie(cookie);
}
}
JwtAuthFilter
package com.example.springsecurityjwt.jwt;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
if (token != null) {
if (!jwtUtil.validateToken(token)) {
log.warn("JWT Token 인증 실패");
throw new IllegalArgumentException("JWT Token 인증 실패");
}
Claims info = jwtUtil.getUserInfoFromToken(token);
setAuthentication(info.getSubject());
}
/*다음 필터 진행*/
filterChain.doFilter(request, response);
}
public void setAuthentication(String username) {
/*jwt 인증 성공 시*/
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = jwtUtil.createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
}
JwtUtil
package com.example.springsecurityjwt.jwt;
import com.example.springsecurityjwt.security.UserRoleEnum;
import com.example.springsecurityjwt.security.UserDetailsServiceImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.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.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_KEY = "auth";
private static final String BEARER_PREFIX = "Bearer=";
private static final long TOKEN_TIME = 60 * 60 * 1000L;
private final UserDetailsServiceImpl userDetailsService;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// header 토큰을 가져오기 -> 헤더검사 없으면 쿠키검사
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
/*헤더에 값이 없다면 토큰 확인*/
if (bearerToken == null) {
Cookie[] cookies = request.getCookies(); // 모든 쿠키 가져오기
if (cookies != null) {
for (Cookie c : cookies) {
String name = c.getName(); // 쿠키 이름 가져오기
String value = c.getValue(); // 쿠키 값 가져오기
if (name.equals(AUTHORIZATION_HEADER)) {
bearerToken = value;
}
}
}
}
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
// 인증 객체 생성
public Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
로직은 아래와 같습니다.
- 회원가입: 암호화 비밀번호, 권한 DB 저장
- 로그인: 아이디 비교, 패스워드 인코더로 입력한 평문 비밀번호와 저장되어 있는 암호화된 비밀번호 비교, 일치한다면 로그인 성공 -> jwt 토큰 쿠키에 저장하여 클라이언트에게 전달
- 쿠키로 저장되기 때문에 매 요청마다 자동으로 쿠키(JWT 토큰)를 서버로 보냄
- 요청마다 서버에 도착한 JWT 토큰의 유효성을 검증하고 로그인 성공 처리
- 인증이 필요한 페이지에 인증이 안된 경우 /login, 인가가 안 된 경우 /forbidden으로 이동
- 로그아웃 시 쿠키에 저장된 토큰 무효화
3. HTML 구현
타임리프에서 제공하는 시큐리티 기능과, 인증 로직이 스프링 서버에서 돌아가기 때문에 프런트엔드 단에서는 쉽습니다.
signUp.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script>
function passwordValidation() {
let memberName = document.getElementById("memberName").value;
let password = document.getElementById("password").value;
let check = /(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^A-Za-z0-9])(?=.{8,})/;
if (check.test(password)) {
return true;
} else {
alert("비밀번호는 최소 하나 이상의 소문자, 하나의 대문자, 하나의 숫자, 하나의 특수문자, 최소 8 자리 입니다.");
return false;
}
}
</script>
<body>
<!--스프링 서버에서는 id가 아닌 name으로 설정해주어야 인식함-->
<div>
<div>SignUp</div>
<form method="POST" action="/api/signup" onsubmit="return passwordValidation()">
<div>MemberName</div>
<input type="text" id="memberName" name="memberName" placeholder="아이디">
<div>Password</div>
<input type="password" id="password" name="password">
<input type="radio" name="role" value="MEMBER" checked>멤버
<input type="radio" name="role" value="PRIVATE_MEMBER">프라이빗 멤버
<input type="radio" name="role" value="VIP_MEMBER">VIP
<input type="radio" name="role" value="ADMIN">관리자
<input type="submit" name="submit" value="회원 가입">
</form>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script>
function loginValidation() {
/*비정상적 접근 거부*/
let memberName = document.getElementById("memberName").value;
let password = document.getElementById("password").value;
//공백만 입력된 경우
var blank_pattern = /^\s+|\s+$/g;
if(
memberName.replace(blank_pattern, '' ) == "" ||
password.replace(blank_pattern, '' ) == ""
){
alert('공백만 입력되었습니다.');
return false; /*폼 전송하지 않음*/
}
//문자열에 공백이 있는 경우
var blank_pattern = /[\s]/g;
if(
blank_pattern.test(memberName) == true ||
blank_pattern.test(password) == true
){
alert('공백이 입력되었습니다.');
return false;
}
//특수문자가 있는 경우
var special_pattern = /[`~!@#$%^&*|\\\'\";:\/?]/gi;
if(special_pattern.test(memberName) == true){
alert('특수문자가 입력되었습니다.');
return false;
}
//공백 혹은 특수문자가 있는 경우
if(memberName.search(/\W|\s/g) > -1){
alert( '특수문자 또는 공백이 입력되었습니다.');
return false;
}
return true;/*폼 전송*/
}
</script>
<body>
<div>
<div>Login</div>
<form method="POST" action="/api/login" onsubmit="return loginValidation()">
<div>MemberName</div>
<input type="text" id="memberName" name="memberName" placeholder="아이디">
<button type="button" onclick="location.href='/signup'">회원가입</button>
<div>Password</div>
<input type="password" id="password" name="password">
<input type="submit" name="submit" value="로그인">
</form>
</div>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>로그인 성공</h1>
<button onclick="location.href='/vip'">VIP 페이지로 이동</button>
<button onclick="location.href='/admin'">Admin 페이지로 이동</button>
<button onclick="location.href='/cookie/test'">cookie test</button>
<button onclick="location.href='/api/logout'">Logout</button>
</body>
</html>
role.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--ADMIN 역할을 갖는다면 이 글이 보임-->
<div sec:authorize="hasRole('ROLE_ADMIN')">
ADMIN <span style="color:green;" sec:authentication="principal.username"></span>님 안녕하세요.
</div>
<!--VIP_MEMBER 역할을 갖는다면 이 글이 보임-->
<div sec:authorize="hasRole('ROLE_VIP_MEMBER')">
VIP_MEMBER <span style="color:green;" sec:authentication="name"></span>님 안녕하세요.
</div>
<div>
소유한 권한들 : <span sec:authentication="principal.authorities"></span>
</div>
<!--어떤 역할이건 상관없이 인증이 되었다면 이 글이 보임-->
<h3 sec:authorize="isAuthenticated()">인증된 사람만 이 글을 볼 수 있습니다.</h3>
<div>
name : <span sec:authentication="name"></span>
</div>
<div>
principal.username : <span sec:authentication="principal.username"></span>
</div>
<div>
principal.password : <span sec:authentication="principal.password"></span>
</div>
<div>
principal.enabled : <span sec:authentication="principal.enabled"></span>
</div>
<div>
principal.accountNonLocked : <span sec:authentication="principal.accountNonLocked"></span>
</div>
<div>
principal.accountNonExpired : <span sec:authentication="principal.accountNonExpired"></span>
</div>
<div>
principal.credentialsNonExpired : <span sec:authentication="principal.credentialsNonExpired"></span>
</div>
</body>
</html>
4. 결과 확인
Github
728x90
반응형
Comments