2021-08-07
This commit is contained in:
@@ -13,6 +13,13 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api
|
||||||
|
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||||
|
|
||||||
|
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl
|
||||||
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||||
|
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-mustache")
|
implementation("org.springframework.boot:spring-boot-starter-mustache")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import kr.pe.elex.examples.model.User;
|
||||||
|
import kr.pe.elex.examples.model.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.CommandLineRunner;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class Application {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserRepository repository;
|
||||||
|
@Autowired
|
||||||
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(Application.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실제 프로젝트에서는 입력 폼을 통해서 회원 가입을 하겠지만, ...
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SetupUsers implements CommandLineRunner {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(String... args) throws Exception {
|
||||||
|
User user = new User("Charlie", "user1", passwordEncoder.encode("pw1"));
|
||||||
|
repository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
import org.springframework.web.filter.GenericFilterBean;
|
||||||
|
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.ServletRequest;
|
||||||
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class JwtFilter extends GenericFilterBean {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private JwtService jwtService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
|
||||||
|
final String authHeader = httpServletRequest.getHeader("Authorization");
|
||||||
|
if (null != authHeader) {
|
||||||
|
try {
|
||||||
|
Authentication authentication = jwtService.getAuthentication(authHeader);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleException(e, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleException(Exception e, ServletResponse response) {
|
||||||
|
final HttpServletResponse httpServletResponse = (HttpServletResponse) response;
|
||||||
|
final ErrorResponse error = new ErrorResponse();
|
||||||
|
if (e instanceof IncorrectClaimException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else if (e instanceof MissingClaimException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else if (e instanceof ExpiredJwtException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else if (e instanceof SignatureException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else if (e instanceof MalformedJwtException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else if (e instanceof UnsupportedJwtException) {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_BAD_REQUEST);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
} else {
|
||||||
|
error.setErrorCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
error.setMessage(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
httpServletResponse.setStatus(error.getErrorCode());
|
||||||
|
httpServletResponse.setContentType(MimeTypeUtils.APPLICATION_JSON_VALUE);
|
||||||
|
httpServletResponse.getWriter().write(mapper.writeValueAsString(error));
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("Oops~", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter @Setter
|
||||||
|
private static class ErrorResponse {
|
||||||
|
@JsonProperty
|
||||||
|
private int errorCode;
|
||||||
|
@JsonProperty
|
||||||
|
private String message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import io.jsonwebtoken.security.SignatureException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class JwtService {
|
||||||
|
|
||||||
|
private final byte[] key = new byte[32];
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
private void init() {
|
||||||
|
new Random().nextBytes(key);
|
||||||
|
log.info("JWT Key = {}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generateToken(final Authentication authentication) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
|
||||||
|
.setIssuer("Elex")
|
||||||
|
.setSubject(authentication.getName())
|
||||||
|
.setExpiration(Date.from(Instant.now().plus(3, ChronoUnit.HOURS)))
|
||||||
|
.claim("authorities", authoritiesToString(authentication.getAuthorities()))
|
||||||
|
.signWith(Keys.hmacShaKeyFor(key))
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jws<Claims> parseToken(final String token)
|
||||||
|
throws UnsupportedJwtException, MalformedJwtException, SignatureException, ExpiredJwtException,
|
||||||
|
MissingClaimException, IncorrectClaimException {
|
||||||
|
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(key)
|
||||||
|
.requireIssuer("Elex") // 토큰의 Issuer 일치 여부 확인
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(parseHeader(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Authentication getAuthentication(String token)
|
||||||
|
throws UnsupportedJwtException, MalformedJwtException, SignatureException, ExpiredJwtException,
|
||||||
|
MissingClaimException, IncorrectClaimException {
|
||||||
|
return getAuthentication(parseToken(token).getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Authentication getAuthentication(Claims claims) {
|
||||||
|
//User principal = (User) userService.loadUserByUsername(claims.getSubject());
|
||||||
|
return new UsernamePasswordAuthenticationToken(claims.getSubject(), claims,
|
||||||
|
authoritiesFromString(claims.get("authorities", String.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String authoritiesToString(Collection<? extends GrantedAuthority> authorities) {
|
||||||
|
StringJoiner joiner = new StringJoiner(",");
|
||||||
|
for (GrantedAuthority authority : authorities) {
|
||||||
|
joiner.add(authority.getAuthority());
|
||||||
|
}
|
||||||
|
return joiner.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection<? extends GrantedAuthority> authoritiesFromString(String authorities) {
|
||||||
|
List<GrantedAuthority> list = new ArrayList<>();
|
||||||
|
for (String authority : authorities.split(",")) {
|
||||||
|
list.add(new SimpleGrantedAuthority(authority));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseHeader(final String authorizationHeader)
|
||||||
|
throws MalformedJwtException {
|
||||||
|
final String[] authentication = authorizationHeader.split(" ");
|
||||||
|
if (authentication.length == 2 && authentication[0].matches("[bB]earer")) {
|
||||||
|
return authentication[1];
|
||||||
|
} else if (authentication.length == 1) {
|
||||||
|
return authentication[0];
|
||||||
|
} else {
|
||||||
|
throw new MalformedJwtException("Authentication Header param must be started with 'Bearer ': " + authorizationHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import kr.pe.elex.examples.model.User;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Controller
|
||||||
|
public class MyController {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인한 사용자 정보를 가져오려면, Authentication을 사용합니다.
|
||||||
|
* 또는, @AuthenticationPrincipal를 사용합니다.
|
||||||
|
*
|
||||||
|
* @param authentication
|
||||||
|
* @param model
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping({"/"})
|
||||||
|
public String index(Authentication authentication, Model model) {
|
||||||
|
if (null != authentication) {
|
||||||
|
User user = (User) (authentication.getPrincipal());
|
||||||
|
log.info("USER: {}", user.getName());
|
||||||
|
|
||||||
|
model.addAttribute("user", user);
|
||||||
|
} else {
|
||||||
|
log.info("NO USER: ");
|
||||||
|
}
|
||||||
|
return "home";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping({"/login"})
|
||||||
|
public String login() {
|
||||||
|
|
||||||
|
return "login";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping({"/info"})
|
||||||
|
public String info() {
|
||||||
|
return "normal_info";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 로그인한 사용자 정보를 가져오기 위해, @AuthenticationPrincipal를 사용합니다.
|
||||||
|
*
|
||||||
|
* @param user
|
||||||
|
* @param model
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@GetMapping({"/secure"})
|
||||||
|
public String secure(@AuthenticationPrincipal User user, Model model) {
|
||||||
|
if (null != user) {
|
||||||
|
log.info("USER: {}", user.getName());
|
||||||
|
} else {
|
||||||
|
log.info("NO USER!!");
|
||||||
|
}
|
||||||
|
model.addAttribute("user", user);
|
||||||
|
return "secure_info";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import kr.pe.elex.examples.model.User;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping({"/api"})
|
||||||
|
public class MyRestController {
|
||||||
|
@Autowired
|
||||||
|
private JwtService jwtService;
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@PostMapping("/signin")
|
||||||
|
public ResponseEntity<Void> signIn(@RequestParam String userId, @RequestParam String userPw) {
|
||||||
|
/*
|
||||||
|
사용자 인증과정을 수동으로 진행
|
||||||
|
*/
|
||||||
|
UsernamePasswordAuthenticationToken authenticationToken =
|
||||||
|
new UsernamePasswordAuthenticationToken(userId, userPw);
|
||||||
|
Authentication authentication = authenticationManager
|
||||||
|
.authenticate(authenticationToken);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
|
||||||
|
/*
|
||||||
|
인증 완료 후 토큰 발행
|
||||||
|
*/
|
||||||
|
String token = jwtService.generateToken(authentication);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header("Authentication", "Bearer " + token)
|
||||||
|
.body(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/user/{userName}")
|
||||||
|
public ResponseEntity<User> getUser(@PathVariable String userName) {
|
||||||
|
return ResponseEntity.ok((User) userService.loadUserByUsername(userName));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Throwable.class)
|
||||||
|
public ResponseEntity<String> onException(Throwable e) {
|
||||||
|
log.error("ERROR!!!", e);
|
||||||
|
return ResponseEntity.internalServerError().body(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
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.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
@Autowired
|
||||||
|
private JwtFilter jwtFilter;
|
||||||
|
//@Autowired
|
||||||
|
//private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(@NotNull HttpSecurity http) throws Exception {
|
||||||
|
http.authorizeRequests()
|
||||||
|
.antMatchers("/", "/info", "/h2-console").permitAll() // 아무나 접근 가능
|
||||||
|
.antMatchers("/h2-console/**").permitAll() // H2콘솔을 쓰기 위해 추가했음
|
||||||
|
.antMatchers("/api/signin").permitAll()
|
||||||
|
.antMatchers("/api/**").authenticated()
|
||||||
|
|
||||||
|
//.antMatchers("/admin").hasAnyRole("ADMIN")
|
||||||
|
.anyRequest().authenticated() // 그 외에는 인증해야 접근 가능
|
||||||
|
/*.and()
|
||||||
|
.formLogin()// 커스텀 로그인 폼
|
||||||
|
.loginPage("/login")
|
||||||
|
.usernameParameter("user_id") // 로그인 폼 매개변수명 지정
|
||||||
|
.passwordParameter("user_pw")
|
||||||
|
.permitAll()
|
||||||
|
.and()
|
||||||
|
.logout()// 로그아웃
|
||||||
|
.logoutSuccessUrl("/")
|
||||||
|
.permitAll()*/
|
||||||
|
//.and().httpBasic();
|
||||||
|
.and().csrf().ignoringAntMatchers("/h2-console/**")// H2콘솔을 쓰기 위해 추가했음
|
||||||
|
.and().headers().frameOptions().sameOrigin()// H2콘솔을 쓰기 위해 추가했음
|
||||||
|
.and().csrf().ignoringAntMatchers("/api/**")
|
||||||
|
.and()
|
||||||
|
//.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
|
||||||
|
//.and()
|
||||||
|
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
|
||||||
|
.sessionManagement()
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(@NotNull AuthenticationManagerBuilder auth) throws Exception {
|
||||||
|
auth.userDetailsService(userService)
|
||||||
|
.passwordEncoder(passwordEncoder())
|
||||||
|
/*auth.inMemoryAuthentication()
|
||||||
|
.withUser("user1")
|
||||||
|
.password("{noop}pw1")
|
||||||
|
.authorities("ROLE_USER")
|
||||||
|
.and()
|
||||||
|
.withUser("user2")
|
||||||
|
.password("{noop}pw2")
|
||||||
|
.authorities("ROLE_USER")*/
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증과정을 수동으로 진행하기 위해서 AuthenticationManager를 빈에 등록 시켜야 한다.
|
||||||
|
* @return
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Override
|
||||||
|
public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||||
|
return super.authenticationManagerBean();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import kr.pe.elex.examples.model.User;
|
||||||
|
import kr.pe.elex.examples.model.UserRepository;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserDetailsService를 구현해서 사용자를 조회할 수 있는 서비스를 만듭니다.
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class UserService implements UserDetailsService {
|
||||||
|
@Autowired
|
||||||
|
private UserRepository repository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
|
||||||
|
final User user = repository.findByUsername(username);
|
||||||
|
if (null == user) {
|
||||||
|
throw new UsernameNotFoundException("Unable to find a user with " + username);
|
||||||
|
} else {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
|
|
||||||
|
import kr.pe.elex.examples.model.User;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
/*
|
||||||
|
@Override
|
||||||
|
public void addViewControllers(@NotNull ViewControllerRegistry registry) {
|
||||||
|
registry.addViewController("/login");
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import javax.persistence.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserDetails를 구현해서 데이터 모델을 만듭니다.
|
||||||
|
*/
|
||||||
|
@Table(name = "User")
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ToString
|
||||||
|
public class User implements UserDetails {
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String name;
|
||||||
|
private String role;
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
public User(String name, String username, String password){
|
||||||
|
this();
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.name = name;
|
||||||
|
this.role = "ROLE_USER";
|
||||||
|
this.enabled = true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return Arrays.asList(new SimpleGrantedAuthority(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonLocked() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCredentialsNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples.model;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
User findByUsername(String username);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
* Spring-boot Examples
|
||||||
|
*
|
||||||
|
* Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
* https://www.elex-project.com/
|
||||||
|
*/
|
||||||
|
|
||||||
|
package kr.pe.elex.examples;
|
||||||
17
security-with-jwt/src/main/resources/application.yaml
Normal file
17
security-with-jwt/src/main/resources/application.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: My spring-boot project
|
||||||
|
mustache:
|
||||||
|
expose-request-attributes: true # 뷰에서 CSRF를 뿌려주기 위해서 필요합니다.
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
enabled: true
|
||||||
|
datasource:
|
||||||
|
url: jdbc:h2:mem:testdb
|
||||||
|
driverClassName: org.h2.Driver
|
||||||
|
username: sa
|
||||||
|
password: pw
|
||||||
|
jpa:
|
||||||
|
database-platform: org.hibernate.dialect.H2Dialect
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
10
security-with-jwt/src/main/resources/banner.txt
Normal file
10
security-with-jwt/src/main/resources/banner.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
('-. ('-. ) (`-.
|
||||||
|
_( OO) _( OO) ( OO ).
|
||||||
|
(,------.,--. (,------.(_/. \_)-.
|
||||||
|
| .---'| |.-') | .---' \ `.' /
|
||||||
|
| | | | OO ) | | \ /\
|
||||||
|
(| '--. | |`-' |(| '--. \ \ |
|
||||||
|
| .--'(| '---.' | .--' .' \_)
|
||||||
|
| `---.| | | `---. / .'. \
|
||||||
|
`------'`------' `------''--' '--'
|
||||||
|
powered by ELEX
|
||||||
43
security-with-jwt/src/main/resources/logback-spring.xml
Normal file
43
security-with-jwt/src/main/resources/logback-spring.xml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
~ Spring-boot Examples
|
||||||
|
~
|
||||||
|
~ Copyright (c) 2021. Elex. All Rights Reserved.
|
||||||
|
~ https://www.elex-project.com/
|
||||||
|
-->
|
||||||
|
|
||||||
|
<configuration>
|
||||||
|
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
|
||||||
|
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
|
||||||
|
|
||||||
|
<springProperty name="LOG_DIR" source="logging.path"
|
||||||
|
defaultValue="${user.home}/logs"/>
|
||||||
|
<property name="LOG_PATH" value="${LOG_DIR}/stephanie.log"/>
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="ROLLING-FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
|
<charset>UTF-8</charset>
|
||||||
|
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
<file>${LOG_PATH}</file>
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>${LOG_DIR}/sebastian_%d{yyyy-MM-dd}_%i.log.gz</fileNamePattern>
|
||||||
|
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
|
||||||
|
<maxFileSize>10MB</maxFileSize>
|
||||||
|
</timeBasedFileNamingAndTriggeringPolicy>
|
||||||
|
<maxHistory>60</maxHistory>
|
||||||
|
</rollingPolicy>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
</configuration>
|
||||||
15
security-with-jwt/src/main/resources/templates/home.mustache
Normal file
15
security-with-jwt/src/main/resources/templates/home.mustache
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<h1>홈</h1>
|
||||||
|
<p>테스트 페이지입니다.</p>
|
||||||
|
{{#user}}
|
||||||
|
안녕, {{name}}.
|
||||||
|
|
||||||
|
<form action="/logout" method="POST">
|
||||||
|
{{! CSRF 토큰이 없으면 에러 남. }}
|
||||||
|
<input type="hidden" name="_csrf" value="{{_csrf.token}}">
|
||||||
|
<button type="submit">로그아웃</button>
|
||||||
|
</form>
|
||||||
|
{{/user}}
|
||||||
|
{{^user}}
|
||||||
|
|
||||||
|
{{/user}}
|
||||||
|
{{> links}}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<div>
|
||||||
|
<a href="/">홈</a> |
|
||||||
|
<a href="/info">일반</a> |
|
||||||
|
<a href="/secure">중요</a> |
|
||||||
|
<a href="/login">로그인</a> |
|
||||||
|
<a href="/h2-console">DB</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<h1>로그인</h1>
|
||||||
|
<form action="/login" method="POST">
|
||||||
|
<label for="user_id">ID:</label>
|
||||||
|
<input type="text" name="user_id" id="user_id"/>
|
||||||
|
<label for="user_pw">PW:</label>
|
||||||
|
<input type="password" name="user_pw" id="user_pw"/>
|
||||||
|
<input type="hidden" name="_csrf" value="{{_csrf.token}}">
|
||||||
|
<input type="submit" value="로그인"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{{> links}}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<h1>아무나 볼 수 있는 페이지</h1>
|
||||||
|
<p>This page doesn't contain any Important messages.</p>
|
||||||
|
{{> links}}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<h1>로그인해야 볼 수 있는 페이지</h1>
|
||||||
|
<p>This page contains a Important message.</p>
|
||||||
|
{{#user}}Hello, {{name}}{{/user}}
|
||||||
|
{{> links}}
|
||||||
Reference in New Issue
Block a user