admin: JWT 기반 인증 방식으로 변경
This commit is contained in:
parent
b81ab51a11
commit
b3e255bda6
|
|
@ -1,8 +1,9 @@
|
||||||
package com.bpgroup.poc.admin.security;
|
package com.bpgroup.poc.admin.security;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.LoginService;
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationFilter;
|
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationFilter;
|
||||||
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationProvider;
|
import com.bpgroup.poc.admin.security.authentication.CustomAuthenticationProvider;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
|
||||||
|
import com.bpgroup.poc.admin.security.jwt.JwtTokenValidatorFilter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
@ -12,8 +13,10 @@ import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||||
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
|
import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter;
|
||||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
|
|
||||||
|
|
@ -32,6 +35,9 @@ public class SecurityConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
|
http.sessionManagement(t -> t.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||||
|
|
||||||
// 보안 기본 설정
|
// 보안 기본 설정
|
||||||
http.headers(c -> c
|
http.headers(c -> c
|
||||||
.frameOptions(fo -> fo.sameOrigin()) // X-Frame-Options: Same Origin
|
.frameOptions(fo -> fo.sameOrigin()) // X-Frame-Options: Same Origin
|
||||||
|
|
@ -50,6 +56,8 @@ public class SecurityConfig {
|
||||||
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리
|
http.formLogin(AbstractHttpConfigurer::disable); // Form 로그인이 아닌 Json 로그인으로 분리
|
||||||
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정
|
http.addFilterBefore(authenticationGenerateFilter(), UsernamePasswordAuthenticationFilter.class); // 로그인 관련 Filter 설정
|
||||||
|
|
||||||
|
http.addFilterAfter(new JwtTokenValidatorFilter(), BasicAuthenticationFilter.class);
|
||||||
|
|
||||||
http.logout(c -> c
|
http.logout(c -> c
|
||||||
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
||||||
.logoutSuccessUrl(LOGIN_PATH)
|
.logoutSuccessUrl(LOGIN_PATH)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.bpgroup.poc.admin.security;
|
||||||
|
|
||||||
|
public class SecurityFilterConstants {
|
||||||
|
public static final String[] EXCLUDE_FILTER_STARTS_WITH_URI = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal"};
|
||||||
|
}
|
||||||
|
|
@ -1,86 +1,41 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.NoArgsConstructor;
|
||||||
import org.springframework.util.LinkedMultiValueMap;
|
|
||||||
import org.springframework.util.MultiValueMap;
|
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
public class AuthenticationDetail {
|
public class AuthenticationDetail {
|
||||||
|
|
||||||
private Long id;
|
|
||||||
private String loginId;
|
private String loginId;
|
||||||
private String name;
|
private String name;
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
private MultiValueMap<MenuOneDepth, MenuTwoDepth> menus = new LinkedMultiValueMap<>();
|
@Builder
|
||||||
|
public static AuthenticationDetail of(String loginId, String name, String email) {
|
||||||
public static AuthenticationDetail from(LoginResult result) {
|
|
||||||
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
||||||
authenticationDetail.id = result.getId();
|
authenticationDetail.loginId = loginId;
|
||||||
authenticationDetail.loginId = result.getLoginId();
|
authenticationDetail.name = name;
|
||||||
authenticationDetail.name = result.getName();
|
authenticationDetail.email = email;
|
||||||
authenticationDetail.email = result.getEmail();
|
|
||||||
|
|
||||||
result.getMenus().forEach(menu -> authenticationDetail.menus.add(
|
|
||||||
MenuOneDepth.of(
|
|
||||||
menu.getMenuGroupUri(),
|
|
||||||
menu.getMenuGroupName(),
|
|
||||||
menu.getMenuGroupSortOrder()
|
|
||||||
),
|
|
||||||
MenuTwoDepth.of(
|
|
||||||
menu.getMenuUri(),
|
|
||||||
menu.getMenuName(),
|
|
||||||
menu.getMenuSortOrder()
|
|
||||||
)
|
|
||||||
));
|
|
||||||
|
|
||||||
return authenticationDetail;
|
return authenticationDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public String toJsonString() {
|
||||||
* EqualsAndHashCode annotattion이 없을 경우 MultiValueMap을 사용할 때 중복된 key를 찾지 못함
|
try {
|
||||||
*/
|
return new ObjectMapper().writeValueAsString(this);
|
||||||
@Getter
|
} catch (JsonProcessingException e) {
|
||||||
@ToString
|
throw new RuntimeException(e);
|
||||||
@EqualsAndHashCode
|
|
||||||
private static class MenuOneDepth {
|
|
||||||
private String uri;
|
|
||||||
private String name;
|
|
||||||
private Integer sortOrder;
|
|
||||||
|
|
||||||
public static MenuOneDepth of(
|
|
||||||
String uri,
|
|
||||||
String name,
|
|
||||||
Integer sortOrder
|
|
||||||
) {
|
|
||||||
MenuOneDepth menu = new MenuOneDepth();
|
|
||||||
menu.uri = uri;
|
|
||||||
menu.name = name;
|
|
||||||
menu.sortOrder = sortOrder;
|
|
||||||
return menu;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Getter
|
public static AuthenticationDetail fromJsonString(String jsonString) {
|
||||||
@ToString
|
try {
|
||||||
private static class MenuTwoDepth {
|
return new ObjectMapper().readValue(jsonString, AuthenticationDetail.class);
|
||||||
private String uri;
|
} catch (JsonProcessingException e) {
|
||||||
private String name;
|
throw new RuntimeException(e);
|
||||||
private Integer sortOrder;
|
|
||||||
|
|
||||||
public static MenuTwoDepth of(
|
|
||||||
String uri,
|
|
||||||
String name,
|
|
||||||
Integer sortOrder
|
|
||||||
) {
|
|
||||||
MenuTwoDepth menu = new MenuTwoDepth();
|
|
||||||
menu.uri = uri;
|
|
||||||
menu.name = name;
|
|
||||||
menu.sortOrder = sortOrder;
|
|
||||||
return menu;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication.exception;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication.exception;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.AdministratorNotFoundException;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.DoNotHaveAnyMenuException;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.InvalidPasswordException;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.AuthenticationFailException;
|
import com.bpgroup.poc.admin.security.authentication.service.LoginResponse;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.LoginRequest;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.AuthenticationFailException;
|
import com.bpgroup.poc.admin.security.authentication.service.LoginResult;
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.AuthenticationFailReason;
|
import com.bpgroup.poc.admin.security.authentication.service.LoginService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
|
@ -30,13 +30,22 @@ public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
||||||
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.authenticated(
|
UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(
|
||||||
result.getLoginId(),
|
result.getLoginId(),
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
authenticationToken.setDetails(AuthenticationDetail.from(result));
|
|
||||||
return authenticationToken;
|
token.setDetails(
|
||||||
|
AuthenticationDetail.builder()
|
||||||
|
.loginId(result.getLoginId())
|
||||||
|
.name(result.getName())
|
||||||
|
.email(result.getEmail())
|
||||||
|
.build()
|
||||||
|
.toJsonString()
|
||||||
|
);
|
||||||
|
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,52 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.LoginResponse;
|
||||||
|
import com.bpgroup.poc.admin.security.jwt.JwtConstants;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import jakarta.servlet.FilterChain;
|
import io.jsonwebtoken.Jwts;
|
||||||
import jakarta.servlet.ServletException;
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
|
||||||
@Override
|
@Override
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException, ServletException {
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
|
||||||
AuthenticationSuccessHandler.super.onAuthenticationSuccess(request, response, chain, authentication);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||||
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
|
||||||
|
// JWT 토큰을 쿠키에 추가
|
||||||
|
String jwtToken = createJwtToken(authentication);
|
||||||
|
Cookie jwtCookie = new Cookie(JwtConstants.JWT_TOKEN_NAME, jwtToken);
|
||||||
|
jwtCookie.setHttpOnly(true);
|
||||||
|
jwtCookie.setPath("/");
|
||||||
|
response.addCookie(jwtCookie);
|
||||||
|
|
||||||
String jsonResponse = new ObjectMapper().writeValueAsString(
|
String jsonResponse = new ObjectMapper().writeValueAsString(
|
||||||
LoginResponse.success(
|
LoginResponse.success()
|
||||||
"token"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
response.getWriter().write(jsonResponse);
|
response.getWriter().write(jsonResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String createJwtToken(Authentication authentication) {
|
||||||
|
SecretKey key = Keys.hmacShaKeyFor(JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
|
||||||
|
return Jwts.builder()
|
||||||
|
.issuer("BP Admin")
|
||||||
|
.subject("Jwt Token")
|
||||||
|
.claim("username", authentication.getName())
|
||||||
|
.claim("details", authentication.getDetails())
|
||||||
|
.issuedAt(new java.util.Date())
|
||||||
|
.expiration(new java.util.Date(System.currentTimeMillis() + JwtConstants.EXPIRATION_TIME))
|
||||||
|
.signWith(key)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
|
||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.ToString;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class LoginResult {
|
|
||||||
private Long id;
|
|
||||||
private String loginId;
|
|
||||||
private String name;
|
|
||||||
private String email;
|
|
||||||
|
|
||||||
private Set<MenuInfo> menus = new HashSet<>();
|
|
||||||
|
|
||||||
public static LoginResult of(Long id, String loginId, String name, String email, Set<MenuInfo> menus) {
|
|
||||||
LoginResult loginResult = new LoginResult();
|
|
||||||
loginResult.id = id;
|
|
||||||
loginResult.loginId = loginId;
|
|
||||||
loginResult.name = name;
|
|
||||||
loginResult.email = email;
|
|
||||||
loginResult.menus = menus;
|
|
||||||
return loginResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@ToString
|
|
||||||
@EqualsAndHashCode
|
|
||||||
public static class MenuInfo {
|
|
||||||
private String menuGroupUri;
|
|
||||||
private String menuGroupName;
|
|
||||||
private Integer menuGroupSortOrder;
|
|
||||||
private String menuUri;
|
|
||||||
private String menuName;
|
|
||||||
private Integer menuSortOrder;
|
|
||||||
|
|
||||||
public static MenuInfo of(
|
|
||||||
String menuGroupUri,
|
|
||||||
String menuGroupName,
|
|
||||||
Integer menuGroupSortOrder,
|
|
||||||
String menuUri,
|
|
||||||
String menuName,
|
|
||||||
Integer menuSortOrder
|
|
||||||
) {
|
|
||||||
MenuInfo menuInfo = new MenuInfo();
|
|
||||||
menuInfo.menuGroupUri = menuGroupUri;
|
|
||||||
menuInfo.menuGroupName = menuGroupName;
|
|
||||||
menuInfo.menuGroupSortOrder = menuGroupSortOrder;
|
|
||||||
menuInfo.menuUri = menuUri;
|
|
||||||
menuInfo.menuName = menuName;
|
|
||||||
menuInfo.menuSortOrder = menuSortOrder;
|
|
||||||
return menuInfo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.AdministratorNotFoundException;
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.DoNotHaveAnyMenuException;
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.exception.InvalidPasswordException;
|
|
||||||
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
|
||||||
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashSet;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class LoginService {
|
|
||||||
|
|
||||||
private final AdministratorRepository administratorRepository;
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public LoginResult login(String username, String password) throws AdministratorNotFoundException, InvalidPasswordException, DoNotHaveAnyMenuException {
|
|
||||||
Optional<Administrator> administrator = administratorRepository.findByLoginId(username);
|
|
||||||
if (administrator.isEmpty()) {
|
|
||||||
throw new AdministratorNotFoundException(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!passwordEncoder.matches(password, administrator.get().getPassword())) {
|
|
||||||
throw new InvalidPasswordException(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LoginResult.of(
|
|
||||||
administrator.get().getId(),
|
|
||||||
administrator.get().getLoginId(),
|
|
||||||
administrator.get().getName(),
|
|
||||||
administrator.get().getLoginId(),
|
|
||||||
getMenus(administrator.get())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private LinkedHashSet<LoginResult.MenuInfo> getMenus(Administrator administrator) throws DoNotHaveAnyMenuException {
|
|
||||||
try {
|
|
||||||
return administrator.getAdministratorRole().getRole().getRoleMenus().stream()
|
|
||||||
.map(roleMenu -> LoginResult.MenuInfo.of(
|
|
||||||
roleMenu.getMenu().getMenuGroup().getUri(),
|
|
||||||
roleMenu.getMenu().getMenuGroup().getName(),
|
|
||||||
roleMenu.getMenu().getMenuGroup().getSortOrder(),
|
|
||||||
roleMenu.getMenu().getUri(),
|
|
||||||
roleMenu.getMenu().getName(),
|
|
||||||
roleMenu.getMenu().getSortOrder()
|
|
||||||
))
|
|
||||||
.sorted(
|
|
||||||
Comparator
|
|
||||||
.comparingInt(LoginResult.MenuInfo::getMenuGroupSortOrder)
|
|
||||||
.thenComparingInt(LoginResult.MenuInfo::getMenuSortOrder)
|
|
||||||
)
|
|
||||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
|
||||||
} catch (NullPointerException e) {
|
|
||||||
throw new DoNotHaveAnyMenuException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication.service;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication;
|
package com.bpgroup.poc.admin.security.authentication.service;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
|
@ -11,11 +11,10 @@ public class LoginResponse {
|
||||||
private String resultMessage;
|
private String resultMessage;
|
||||||
private String token;
|
private String token;
|
||||||
|
|
||||||
public static LoginResponse success(String token) {
|
public static LoginResponse success() {
|
||||||
LoginResponse response = new LoginResponse();
|
LoginResponse response = new LoginResponse();
|
||||||
response.resultCode = "0000";
|
response.resultCode = "0000";
|
||||||
response.resultMessage = "Success";
|
response.resultMessage = "Success";
|
||||||
response.token = token;
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.bpgroup.poc.admin.security.authentication.service;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class LoginResult {
|
||||||
|
private Long id;
|
||||||
|
private String loginId;
|
||||||
|
private String name;
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
public static LoginResult of(Long id, String loginId, String name, String email) {
|
||||||
|
LoginResult loginResult = new LoginResult();
|
||||||
|
loginResult.id = id;
|
||||||
|
loginResult.loginId = loginId;
|
||||||
|
loginResult.name = name;
|
||||||
|
loginResult.email = email;
|
||||||
|
return loginResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.bpgroup.poc.admin.security.authentication.service;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.domain.admin.entity.Administrator;
|
||||||
|
import com.bpgroup.poc.admin.domain.admin.entity.AdministratorRepository;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.AdministratorNotFoundException;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.DoNotHaveAnyMenuException;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.service.exception.InvalidPasswordException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LoginService {
|
||||||
|
|
||||||
|
private final AdministratorRepository administratorRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public LoginResult login(String username, String password) throws AdministratorNotFoundException, InvalidPasswordException, DoNotHaveAnyMenuException {
|
||||||
|
Optional<Administrator> administrator = administratorRepository.findByLoginId(username);
|
||||||
|
if (administrator.isEmpty()) {
|
||||||
|
throw new AdministratorNotFoundException(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!passwordEncoder.matches(password, administrator.get().getPassword())) {
|
||||||
|
throw new InvalidPasswordException(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoginResult.of(
|
||||||
|
administrator.get().getId(),
|
||||||
|
administrator.get().getLoginId(),
|
||||||
|
administrator.get().getName(),
|
||||||
|
administrator.get().getLoginId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication.exception;
|
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||||
|
|
||||||
public class AdministratorNotFoundException extends Exception {
|
public class AdministratorNotFoundException extends Exception {
|
||||||
public AdministratorNotFoundException(String message) {
|
public AdministratorNotFoundException(String message) {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication.exception;
|
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||||
|
|
||||||
public class DoNotHaveAnyMenuException extends Exception {
|
public class DoNotHaveAnyMenuException extends Exception {
|
||||||
public DoNotHaveAnyMenuException() {
|
public DoNotHaveAnyMenuException() {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.security.authentication.exception;
|
package com.bpgroup.poc.admin.security.authentication.service.exception;
|
||||||
|
|
||||||
public class InvalidPasswordException extends Exception {
|
public class InvalidPasswordException extends Exception {
|
||||||
public InvalidPasswordException(String message) {
|
public InvalidPasswordException(String message) {
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package com.bpgroup.poc.admin.security.jwt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: 사용 시 별도 property 파일로 관리 필요
|
||||||
|
*/
|
||||||
|
public class JwtConstants {
|
||||||
|
public static final String JWT_KEY = "8530b13adb4e420d9694b27570635b47";
|
||||||
|
public static final String JWT_TOKEN_NAME = "JWT-TOKEN";
|
||||||
|
public static final long EXPIRATION_TIME = 60 * 10 * 1000;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package com.bpgroup.poc.admin.security.jwt;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.security.SecurityFilterConstants;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.AuthenticationDetail;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.Cookie;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class JwtTokenValidatorFilter extends OncePerRequestFilter {
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||||
|
String jwt = getJwtFromCookie(request);
|
||||||
|
if (null != jwt) {
|
||||||
|
try {
|
||||||
|
SecretKey key = Keys.hmacShaKeyFor(
|
||||||
|
JwtConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
Claims claims = Jwts.parser()
|
||||||
|
.verifyWith(key)
|
||||||
|
.build()
|
||||||
|
.parseSignedClaims(jwt)
|
||||||
|
.getPayload();
|
||||||
|
String username = claims.get("username", String.class);
|
||||||
|
String details = claims.get("details", String.class);
|
||||||
|
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
|
||||||
|
auth.setDetails(AuthenticationDetail.fromJsonString(details));
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new BadCredentialsException("Invalid Jwt Token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getJwtFromCookie(HttpServletRequest request) {
|
||||||
|
Cookie[] cookies = request.getCookies();
|
||||||
|
if (null != cookies) {
|
||||||
|
for (Cookie cookie : cookies) {
|
||||||
|
if (JwtConstants.JWT_TOKEN_NAME.equals(cookie.getName())) {
|
||||||
|
return cookie.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
|
List<String> paths = List.of(SecurityFilterConstants.EXCLUDE_FILTER_STARTS_WITH_URI);
|
||||||
|
return paths.stream().anyMatch(request.getRequestURI()::startsWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
package com.bpgroup.poc.admin.web.advice.menu;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class MenuInfo {
|
||||||
|
|
||||||
|
private String uri;
|
||||||
|
private String name;
|
||||||
|
private Integer sortOrder;
|
||||||
|
|
||||||
|
List<MenuChild> menuChildren = new ArrayList<>();
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
public static MenuInfo of(String uri, String name, Integer sortOrder) {
|
||||||
|
MenuInfo menuInfo = new MenuInfo();
|
||||||
|
menuInfo.uri = uri;
|
||||||
|
menuInfo.name = name;
|
||||||
|
menuInfo.sortOrder = sortOrder;
|
||||||
|
return menuInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addMenuChild(MenuChild menuChild) {
|
||||||
|
menuChildren.add(menuChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@Builder
|
||||||
|
public static class MenuChild {
|
||||||
|
private String uri;
|
||||||
|
private String name;
|
||||||
|
private Integer sortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.bpgroup.poc.admin.web.advice.menu;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@ControllerAdvice(basePackages = "com.bpgroup.poc.admin.web.main")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuInfoControllerAdvice {
|
||||||
|
|
||||||
|
private final MenuQueryRepository menuQueryRepository;
|
||||||
|
|
||||||
|
@ModelAttribute("menuInfos")
|
||||||
|
public List<MenuInfo> menuInfo() {
|
||||||
|
return menuQueryRepository.findAllByLoginId();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package com.bpgroup.poc.admin.web.advice.menu;
|
||||||
|
|
||||||
|
import com.querydsl.core.Tuple;
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QAdministrator.administrator;
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QAdministratorRole.administratorRole;
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QMenu.menu;
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QMenuGroup.menuGroup;
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QRole.role;
|
||||||
|
import static com.bpgroup.poc.admin.domain.admin.entity.QRoleMenu.roleMenu;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MenuQueryRepository {
|
||||||
|
|
||||||
|
private final JPAQueryFactory queryFactory;
|
||||||
|
|
||||||
|
public List<MenuInfo> findAllByLoginId() {
|
||||||
|
List<Tuple> results = queryFactory.select(
|
||||||
|
menuGroup.uri,
|
||||||
|
menuGroup.name,
|
||||||
|
menuGroup.sortOrder,
|
||||||
|
roleMenu.menu.uri,
|
||||||
|
roleMenu.menu.name,
|
||||||
|
roleMenu.menu.sortOrder
|
||||||
|
)
|
||||||
|
.from(administrator)
|
||||||
|
.innerJoin(administrator.administratorRole, administratorRole)
|
||||||
|
.innerJoin(administratorRole.role, role)
|
||||||
|
.innerJoin(role.roleMenus, roleMenu)
|
||||||
|
.innerJoin(roleMenu.menu, menu)
|
||||||
|
.innerJoin(menu.menuGroup, menuGroup)
|
||||||
|
.where(administrator.loginId.eq("admin"))
|
||||||
|
.orderBy(menuGroup.sortOrder.asc(), roleMenu.menu.sortOrder.asc())
|
||||||
|
.fetch();
|
||||||
|
|
||||||
|
LinkedHashMap<String, MenuInfo> menuInfoMap = makeMenuForm(results);
|
||||||
|
return new ArrayList<>(menuInfoMap.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkedHashMap<String, MenuInfo> makeMenuForm(List<Tuple> results) {
|
||||||
|
LinkedHashMap<String, MenuInfo> menuInfoMap = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
for (Tuple tuple : results) {
|
||||||
|
String menuGroupUri = tuple.get(menuGroup.uri);
|
||||||
|
MenuInfo menuInfo = menuInfoMap.get(menuGroupUri);
|
||||||
|
|
||||||
|
if (menuInfo == null) {
|
||||||
|
menuInfo = MenuInfo.builder()
|
||||||
|
.uri(tuple.get(menuGroup.uri))
|
||||||
|
.name(tuple.get(menuGroup.name))
|
||||||
|
.sortOrder(tuple.get(menuGroup.sortOrder))
|
||||||
|
.build();
|
||||||
|
menuInfoMap.put(menuGroupUri, menuInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuInfo.MenuChild menuChild = MenuInfo.MenuChild.builder()
|
||||||
|
.uri(tuple.get(roleMenu.menu.uri))
|
||||||
|
.name(tuple.get(roleMenu.menu.name))
|
||||||
|
.sortOrder(tuple.get(roleMenu.menu.sortOrder))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
menuInfo.addMenuChild(menuChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
return menuInfoMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.bpgroup.poc.admin.web.advice;
|
package com.bpgroup.poc.admin.web.advice.path;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
@ -7,13 +7,13 @@
|
||||||
<h2>메뉴</h2>
|
<h2>메뉴</h2>
|
||||||
<button class="closeButton"><img th:src="@{/images/ico_close.svg}"></button>
|
<button class="closeButton"><img th:src="@{/images/ico_close.svg}"></button>
|
||||||
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
<div th:if="${#authorization.expression('isAuthenticated()')}">
|
||||||
<div th:each="entry : ${#authentication.details.getMenus().entrySet()}">
|
<div th:each="menuGroup : ${menuInfos}">
|
||||||
<a class="nav_tit"
|
<a class="nav_tit"
|
||||||
th:classappend="${pathInfo.activeClass(entry.key.getUri())}"
|
th:classappend="${pathInfo.activeClass(menuGroup.getUri())}"
|
||||||
th:text="${entry.key.getName()}">
|
th:text="${menuGroup.getName()}">
|
||||||
</a>
|
</a>
|
||||||
<ul class="nav_section">
|
<ul class="nav_section">
|
||||||
<li th:each="menu : ${entry.value}">
|
<li th:each="menu : ${menuGroup.getMenuChildren()}">
|
||||||
<a th:with="path= ${menu.getUri()}" th:href="${path}"
|
<a th:with="path= ${menu.getUri()}" th:href="${path}"
|
||||||
th:classappend="${pathInfo.activeClass(path)}"><span th:text="${menu.getName()}"></span></a>
|
th:classappend="${pathInfo.activeClass(path)}"><span th:text="${menu.getName()}"></span></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue