끄적끄적
[spring] security와 JWT 사용하기 본문
학습한 내용을 정리한 것입니다.
생략된 내용들과 부정확할 수 있는 내용들에 주의해주세요.
- 구조도
- security 설정
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SignInProvider signInProvider;
@Autowired
SignInFilterSuccessHandler signInFilterSuccessHandler;
@Autowired
SignInFilterFailureHandler signInFilterFailureHandler;
@Autowired
JwtProvider jwtProvider;
@Autowired
JwtFilterSuccessHandler jwtFilterSuccessHandler;
@Autowired
JwtFilterFailureHandler jwtFilterFailureHandler;
//filter 생성 및 Manager에 등록
@Bean
public JwtFilter jwtFilter() throws Exception {
FilterSkipMatcher filterSkipMatcher = new FilterSkipMatcher(
Arrays.asList(
"/api/account/signIn",
"/api/account/signOut",
"/api/account/authenticate"), "/api/**");
JwtFilter jwtFilter = new JwtFilter(filterSkipMatcher, jwtFilterSuccessHandler, jwtFilterFailureHandler);
jwtFilter.setAuthenticationManager(super.authenticationManagerBean());
return jwtFilter;
}
@Bean
public SignInFilter signInFilter() throws Exception{
SignInFilter signInFilter = new SignInFilter("/api/account/signIn",signInFilterSuccessHandler, signInFilterFailureHandler);
signInFilter.setAuthenticationManager(super.authenticationManagerBean());
return signInFilter;
}
@Bean
public SignOutFilter signOutFilter() throws Exception{
SignOutFilter signOutFilter = new SignOutFilter("/api/account/signOut");
signOutFilter.setAuthenticationManager(super.authenticationManagerBean());
return signOutFilter;
}
//Manager 등록
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception{
return super.authenticationManagerBean();
}
//Provider 등록
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(this.jwtProvider)
.authenticationProvider(this.signInProvider);
}
//Filter 등록
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.csrf().disable();
http
.headers().frameOptions().disable();
http
.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(signInFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(signOutFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
사용자의 request를 어떤 filter가 처리할 것인지, 어떤 provider를 manager에 등록할 것인지 등의 설정입니다.
- token
- UsernamePasswordAuthenticationToken은 세큐리티가 디폴트로 제공하는 Authentication 인터페이스의 구현체
- UsernamePasswordAuthenticationToken을 상속받는 커스텀 토큰을 생성하여 목적에 따라 구분
- 이 token의 클래스타입을 보고 어떤 provider가 처리할 것인지 판단
- UsernamePasswordAuthenticationToken를 생성자2개로 생성하면 인증받기 전 토큰을 뜻함
- UsernamePasswordAuthenticationToken를 생성자3개로 생성하면 인증받은 후 토큰을 뜻함
여기서는 아래의 3가지 토큰을 정의
- SignInPreToken(로그인 전)
public class SignInPreToken extends UsernamePasswordAuthenticationToken {
public SignInPreToken(String email, String password) {
super(email, password);
}
public SignInPreToken(AccountDTO.SignIn dto) {
this(dto.getEmail(), dto.getPassword());
}
public String getEmail(){
return (String)super.getPrincipal();
}
public String getPassword(){
return (String)super.getCredentials();
}
}
- SignInPostToken(로그인 후)
public class SignInPostToken extends UsernamePasswordAuthenticationToken {
public SignInPostToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
public String getEmail(){
return (String)super.getPrincipal();
}
public String getPassword(){
return (String)super.getCredentials();
}
public AccountSecurityDTO getAccountSecurityDTO() {
return (AccountSecurityDTO) super.getPrincipal();
}
public AccountDetails getAccountDetails(){
return new AccountDetails(this.getEmail(), this.getPassword(), super.getAuthorities());
}
}
- JwtPreToken(서버 REST API 요청),
public class JwtPreToken extends UsernamePasswordAuthenticationToken {
public JwtPreToken(String token) {
super(token, token.length());
}
public String getToken(){
return (String)super.getPrincipal();
}
public int getTokenLength(){
return getToken().length();
}
}
- filter
- SignInFilter
public class SignInFilter extends AbstractAuthenticationProcessingFilter {
private SignInFilterSuccessHandler successHandler;
private SignInFilterFailureHandler failureHandler;
public SignInFilter(String defaultUrl){
super(defaultUrl);
}
public SignInFilter(String defaultUrl,
SignInFilterSuccessHandler successHandler,
SignInFilterFailureHandler failureHandler){
this(defaultUrl);
this.successHandler = successHandler;
this.failureHandler = failureHandler;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
AccountDTO.SignIn dto = new ObjectMapper().readValue(req.getReader(),AccountDTO.SignIn.class);
SignInPreToken token = new SignInPreToken(dto);
return super.getAuthenticationManager().authenticate(token);
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException {
this.successHandler.onAuthenticationSuccess(req,res,authResult);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException {
this.failureHandler.onAuthenticationFailure(req,res,failed);
}
}
@Override한 attempAuthentication 메소드에서 req에서 로그인 정보를 얻고, 그에 따라 SignInPreToken을 생성하여 ProviderManager에게 넘깁니다.
successfulAuthentication과 unsuccessfulAuthentication은 각각 필터가 요청을 성공,실패 했을때 동작하는 메소드들입니다.
아래 코드는 로그인이 성공했을때의 로직입니다. (this.successHandler.onAuthenticationSuccess 의 내용)
@Component
public class SignInFilterSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtFactory jwtFactory;
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws IOException, ServletException {
String jwt = jwtFactory.generateToken(((SignInPostToken) auth).getAccountSecurityDTO());
Cookie cookie = new Cookie("jwt-token", jwt);
cookie.setPath("/");
cookie.setMaxAge(60*60);
res.addCookie(cookie);
}
}
jwtFactory를 통해 토큰을 생성하고, 쿠키에 담아 클라이언트에게 응답해줍니다.
아래는 JwtFactory 입니다.
@Slf4j
@Component
public class JwtFactory {
@Value("${jwt.secret}")
private String secret;
public String generateToken(AccountSecurityDTO dto) {
String token = null;
try {
token = JWT.create()
.withIssuer("kong")
.withClaim("email", dto.getEmail())
.withClaim("nickname", dto.getNickname())
.withClaim("role", dto.getRole().toString())
.withClaim("gen-time", new Date())
.sign(generateAlgorithm());
} catch (Exception e) {
log.error(e.getMessage());
}
return token;
}
private Algorithm generateAlgorithm() throws UnsupportedEncodingException {
return Algorithm.HMAC256(secret);
}
}
- SignOutFilter
public class SignOutFilter extends AbstractAuthenticationProcessingFilter {
public SignOutFilter(String defaultUrl){
super(defaultUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
res.addCookie(JwtCookieUtil.createSignOutCookie());
SecurityContextHolder.clearContext();
return null;
}
}
@Override한 attempAuthentication 메소드에서 로그아웃처리를 하고있다. 여기서는 jwt토큰을 쿠키에 담아서 사용함으로, 해당 쿠키를 제거함으로써(위 코드에서는 max-age가 0인 쿠키를 넣어줌으로써) 로그아웃을 구현하고 있다.
- JwtFilter
public class JwtFilter extends AbstractAuthenticationProcessingFilter {
private JwtFilterSuccessHandler successHandler;
private JwtFilterFailureHandler failureHandler;
public JwtFilter(RequestMatcher requestMatcher){
super(requestMatcher);
}
public JwtFilter(RequestMatcher requestMatcher,
JwtFilterSuccessHandler successHandler,
JwtFilterFailureHandler failureHandler){
this(requestMatcher);
this.successHandler = successHandler;
this.failureHandler = failureHandler;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException, IOException, ServletException {
String jwtToken = JwtCookieUtil.getJwtCookieValue(req);
return super.getAuthenticationManager().authenticate(new JwtPreToken(jwtToken));
}
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication authResult) throws IOException, ServletException {
this.successHandler.onAuthenticationSuccess(req,res,authResult);
chain.doFilter(req,res);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse res, AuthenticationException failed) throws IOException, ServletException {
this.failureHandler.onAuthenticationFailure(req,res,failed);
}
}
@Override한 attempAuthentication 메소드에서 jwt토큰이 담겨있는 쿠키를 추출하고, jwt문자열을 얻어낸다.
그리고 그것을 JwtPreToken에 담아 JwtProvider에서 처리하도록 넘겨준다.
그리고 chain.doFilter(req,res)를 통해 요청이 디스패처 서블릿까지 도달하도록 해줍니다.
아래는 jwt토큰의 유효성 검사가 성공할때 작동하는, @Override successfulAuthentication 메소드의 로직입니다.
(this.successHandler.onAuthenticationSuccess의 로직)
@Component
public class JwtFilterSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(auth);
SecurityContextHolder.setContext(context);
}
}
- provider
- SignInProvider
@Component
public class SignInProvider implements AuthenticationProvider {
@Autowired
AccountDetailsService detailsService;
@Autowired
PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SignInPreToken token = (SignInPreToken) authentication;
AccountDetails details = (AccountDetails) detailsService.loadUserByUsername(token.getEmail());
details.validatePreToken(token,passwordEncoder);
return details.getPostToken(details);
}
@Override
public boolean supports(Class<?> aClass) {
return SignInPreToken.class.isAssignableFrom(aClass) ;
}
}
SignInFilter에게 넘겨받은 토큰을 통해 이메일을 알아냅니다.
해당 이메일을 통해 DB에서 PASSWORD를 알아내고, 로그인이 가능한지 판단합니다.
로그인이 가능하다면, SignInPostToken(details.getPostToken)을 생성하여 SignInFilter에 다시 넘겨줍니다.
- JwtProvider
@Component
public class JwtProvider implements AuthenticationProvider {
@Autowired
JwtDecoder jwtDecoder;
@Autowired
AccountDetailsService detailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
JwtPreToken token = (JwtPreToken) authentication;
if(isAnonymous(token)){
return token;
}
DecodedJWT decodedJWT = jwtDecoder.decodeJwt(token.getToken());
SignInPostToken signInPostToken = (SignInPostToken) SecurityContextHolder.getContext().getAuthentication();
if(signInPostToken == null){
String email = decodedJWT.getClaim("email").asString();
AccountDetails details = (AccountDetails) detailsService.loadUserByUsername(email);
return details.getPostToken(details);
}
return signInPostToken;
}
@Override
public boolean supports(Class<?> aClass) {
return JwtPreToken.class.isAssignableFrom(aClass);
}
private boolean isAnonymous(JwtPreToken token){
return token.getToken().equals("");
}
}
JwtFilter에게 넘겨받은 토큰을 통해 jwt-token 문자열을 알아냅니다.
해당 문자열을 jwtDecoder를 통해 유효성을 검증하고, 문제가 없다면 SignInPostToken을 생성하여 JwtFilter에게 다시 돌려줍니다.
* 현재 위 로직은 적절치 않습니다. (매번 db에서 정보를 쿼리해와야합니다)
JwtPreToken안에 있는 정보만으로 SignInPostToken을 생성할 수 있도록 로직을 변경하여야합니다.
(따라서, JwtFactory의 로직변경 필요)
아래는 JwtDecoder입니다.
@Slf4j
@Component
public class JwtDecoder {
@Autowired
AccountDetailsService detailsService;
@Value("${jwt.secret}")
private String secret;
public DecodedJWT decodeJwt(String token) {
return isValidToken(token).orElseThrow(() -> new InvalidJwtException("유효한 토큰아 아닙니다."));
}
private Optional<DecodedJWT> isValidToken(String token) {
DecodedJWT jwt = null;
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm).build();
jwt = verifier.verify(token);
} catch (Exception e) {
log.error(e.getMessage());
}
return Optional.ofNullable(jwt);
}
}
git : https://github.com/DingDongDeng/MyBoard
'개발' 카테고리의 다른 글
[spring] database 연결 테스트 코드 (0) | 2019.12.24 |
---|---|
[spring] properties 여러개 사용하기 (0) | 2019.12.24 |
[database] 자주 쓰는 SQL (0) | 2019.12.24 |
[intelij] 인텔리제이 환경설정하기 (0) | 2019.12.24 |
[spring] @Valid 활용하기 (0) | 2019.12.23 |