Spring Framework-18

유호준·2021년 4월 4일
0

Spring Framework

목록 보기
19/21
post-thumbnail

이번엔 form 로그인과 세션이 아니라 JWT방식으로 변경해보자!

JWTJSON Web Token의 준말입니다. 현재는 웹이지만 후에 앱과 통신할 때 이를 사용해 인증할 수 있습니다. 보안을 위해서 짧은 Access Token을 사용해 통신하고 만료시간이 지나면 Refresh Token으로 토큰을 재발급 받는 것이 안전하나 여기서는 Access Token으로 인증하는 부분만 구현하겠습니다.

코드 참고: https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/


pom.xml 수정

JWT에 필요한 라이브러리를 추가합니다.

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

JWTTokenProvier 클래스 생성

JWT의 전반적인 기능을 담당하는 클래스입니다. JWT토큰은 비밀키와 함께 인코딩 됩니다. 여기서 비밀키도 문자열 그대로가 아니라 인코딩 되있어야 합니다. JWTTokenProvider는 토큰을 생성하고, 토큰으로 Authentication 객체를 만들고, Header에서 토큰을 반환하고, 토큰을 검증하는 역할을 합니다. 토큰은 idrole을 저장하고 있도록 생성합니다.

@Component
@RequiredArgsConstructor
public class JWTTokenProvider {
    private final String SECRET_KEY = "secret";
    private final String ENCODED_SECRET_KEY = Base64.getEncoder().encodeToString(SECRET_KEY.getBytes(StandardCharsets.UTF_8));
    private final long TOKEN_VALID_MILISECOND = 1000L * 60 * 60; //1시간
    private final UserDetailsService userDetailsService;

    public String createToken(int id, String role){
        Date now = new Date();
        Claims claims = Jwts.claims().setSubject(String.valueOf(id));

        claims.put("role",role);

        return Jwts.builder().setClaims(claims).setIssuedAt(now)
                .setExpiration(new Date(now.getTime() + TOKEN_VALID_MILISECOND)).signWith(SignatureAlgorithm.HS256,ENCODED_SECRET_KEY).compact();
    }

    public Authentication getAuthentication(String token){
        UserDetails userDetails = userDetailsService.loadUserByUsername(getId(token));
        return new UsernamePasswordAuthenticationToken(userDetails,"",userDetails.getAuthorities());
    }

    public String getId(String token){
        return Jwts.parser().setSigningKey(ENCODED_SECRET_KEY).parseClaimsJws(token).getBody().getSubject();
    }

    public String resolveToken(HttpServletRequest request){
        return request.getHeader("X-AUTH-TOKEN");
    }

    public boolean validateToken(String token){
        try{
            Jws<Claims> claims = Jwts.parser().setSigningKey(ENCODED_SECRET_KEY).parseClaimsJws(token);
            return claims.getBody().getExpiration().after(new Date());
        }catch (Exception e){
            return false;
        }
    }
}

JWTAuthenticationFilter

이 필터는 Header에서 토큰이 있는지 있다면 유효한 토큰인지 확인하고 맞다면 Authentication을 등록하는 필터입니다.

public class JWTAuthenticationFilter extends GenericFilterBean {
    private JWTTokenProvider tokenProvider;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        if(tokenProvider==null){
            tokenProvider = WebApplicationContextUtils.getWebApplicationContext(servletRequest.getServletContext())
                    .getBean(JWTTokenProvider.class);
        }

        String token = tokenProvider.resolveToken((HttpServletRequest) servletRequest);
        if(token != null && tokenProvider.validateToken(token)){
            Authentication authentication = tokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }
}

SecurtiyConfig 수정

기존 코드를 주석처리하고 수정합니다. JWT를 사용해 인증하므로 form으로 로그인을 하지 않도록 비활성화합니다. 또한 세션을 사용하지 않으므로 세션을 만들지 않도록 설정합니다. 마지막 addFilterBefore가 중요한데 JWTAuthenticationFilter를 로그인을 수행하는UsernamePasswordAuthenticationFilter 이전에 두어서 이 필터를 거치기 전에 우리가 위에서 만들었던 필터를 거쳐 JWT를 이용해 AuthenticationSecurityContext에 등록하므로 이를 거치지 않고 통과합니다.

@Override
    protected void configure(HttpSecurity http) throws Exception {
//        http.httpBasic().and().formLogin().loginPage("/").loginProcessingUrl("/login").defaultSuccessUrl("/board",true)
//                .and().logout().deleteCookies("JSESSIONID").invalidateHttpSession(true)
//                .and().authorizeRequests()
//                .antMatchers("/vendor/**","/css/**","/js/**","/scss/**","/img/**","/","/user").permitAll()
//                .anyRequest().authenticated().and().csrf().disable();

        http.httpBasic().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and().authorizeRequests()
                .antMatchers("/vendor/**","/css/**","/js/**","/scss/**","/img/**","/","/user").permitAll()
                .anyRequest().authenticated().and().csrf().disable()
                .addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

    }

UserMapper 인터페이스와 xml 수정

iduser를 조회할 수 있도록 수정합니다.

public interface UserMapper {
    public void save(UserVO userVO);
    public int checkEmailDuplication(String email);
    public UserVO findByEmail(String email);
    public UserVO findById(int id);
}
<mapper namespace="ac.kr.smu.mapper.UserMapper">
    <insert id="save">
        INSERT INTO user(name,email,password)
        VALUES (#{name},#{email},#{password})
    </insert>

    <select id="checkEmailDuplication" resultType="int">
        SELECT COUNT(*) FROM user where email=#{email}
    </select>

    <select id="findByEmail" resultType="UserVO">
        SELECT * FROM user where email=#{email}
    </select>

    <select id="findById" resultType="UserVO">
        SELECT * FROM user where id=#{id}
    </select>
</mapper>

UserService 수정

기존에 주석처리 했던 checkPassword 메소드를 수정합니다. 우리가 빈으로 등록한 PasswordEncoder를 사용하여 비밀번호를 비교하도록 수정합니다.

public interface UserService {
    public void save(UserVO userVO);
    public boolean checkEmailDuplication(String email);
    public boolean checkPassword(String email,String password);
    public UserVO findByEmail(String email);
}
@RequiredArgsConstructor
@Service
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;
    private final PasswordEncoder passwordEncoder;

    @Override
    public void save(UserVO userVO) {
        userVO.setPassword(passwordEncoder.encode(userVO.getPassword()));
        userMapper.save(userVO);
    }

    @Override
    public boolean checkEmailDuplication(String email) {
        return userMapper.checkEmailDuplication(email)==0;
    }

    @Override
    public boolean checkPassword(String email, String password) {
        return passwordEncoder.matches(password,userMapper.findByEmail(email).getPassword());
    }

    @Override
    public UserVO findByEmail(String email) {
        return userMapper.findByEmail(email);
    }

}

CustomUserDetailService 수정

email로 조회하는 부분을 주석처리하고 id로 조회하도록 수정합니다.

@Service
@RequiredArgsConstructor
public class CustomUserDetailServiceImpl implements UserDetailsService {
    private final UserMapper userMapper;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//        return userMapper.findByEmail(s);
        return userMapper.findById(Integer.valueOf(s));
    }
}

LoginController 수정

로그인을 처리하는 부분을 만듭니다. 비밀번호를 비교하여서 맞다면 email로 유저의 정보를 모두 받아온 후 이를 이용하여 토큰을 만듭니다. 비밀번호가 올바르지 않다면 tokennull을 넣어 반합니다.

@RequiredArgsConstructor
@Controller
@RequestMapping("/")
public class LoginController {

    private final UserService userService;
    private final JWTTokenProvider tokenProvider;

    @GetMapping
    public String getLogin(){
        return "login";
    }

    @PostMapping
    public @ResponseBody ResponseEntity<?> postLogin(@RequestBody UserVO userVO){
        Map<String, String> body = new HashMap<>();

        if(userService.checkPassword(userVO.getEmail(),userVO.getPassword())) {
            UserVO user = userService.findByEmail(userVO.getEmail());
            body.put("token", tokenProvider.createToken(user.getId(), ((List<GrantedAuthority>) user.getAuthorities()).get(0).getAuthority()));
        }
        else
            body.put("token",null);

        return ResponseEntity.ok(body);

    }
//
//    @PostMapping("/logout")
//    public void logout(HttpSession session){
//        session.invalidate();
//    }
}

테스트

테스트는 postman과 같은 도구를 이용하여 진행합니다.



인증에 성공해서 board.jsp의 윗부분이 나온 것을 확인할 수 있습니다.

0개의 댓글