
JWT는 사용자 인증 및 식별을 위한 토큰(Token)기반 인증이다. 토큰 자체에 사용자의 권한 정보나 서비스를 사용하기 위한 정보가 포함되어 있다.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
토큰 유효성 확인을 위한 Secret Key 설정 (외부에 노출되지 않도록 주의!!!)
jwt.signingkey=test)*&^%%#$
먼저 JWT 관련 Security 설정부터 해보자
SecurityConfiguration.Java
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint unauthorizedHandler;
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().
authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
}
configure에 설정한 내용을 보면
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
JwtAuthenticationEntryPoint는 AuthenticationEntryPoint를 구현하여 인증에 실패한 사용자의 response에 HttpServletResponse.SC_UNAUTHORIZED를 담아주도록 구현한다.
@SuppressWarnings("serial")
@Component
public class JwtTokenUtil implements Serializable {
@Value("${jwt.signingkey}")
private String SIGNING_KEY;
private int ACCESS_TOKEN_VALIDITY_SECONDS = 24 * 60 * 60;
public String getUserIdFromToken(String token) {
Claims claims = getClaimFromToken(token);
return claims.get("user_id").toString();
}
public String getAdminAuthFromToken(String token) {
Claims claims = getClaimFromToken(token);
return claims.get("auth").toString();
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public Claims getClaimFromToken(String token) {
final Claims claims = getAllClaimsFromToken(token);
return claims;
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
if (expiration == null) {
return false;
}
return expiration.before(new Date());
}
public String generateToken(UserModel userModel) {
return doGenerateToken(userModel,"user");
}
private String doGenerateToken(UserModel userModel, String subject) {
Claims claims = Jwts.claims();
claims.put("user_id", userModel.getUser_id());
claims.put("auth", "");
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_SELLER")));
return Jwts.builder()
.setSubject(subject)
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
.compact();
}
private String doGenerateTokenForAdmin(AdminModel csModel, String subject) {
Claims claims = Jwts.claims();
claims.put("user_id", csModel.getId());
claims.put("auth", csModel.getAuth());
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_SELLER")));
return Jwts.builder()
.setSubject(subject)
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_VALIDITY_SECONDS*1000))
.compact();
}
private String doGenerateTokenForDriver(AdminModel csModel, String subject) {
Claims claims = Jwts.claims();
claims.put("user_id", csModel.getId());
claims.put("auth", csModel.getAuth());
claims.put("scopes", Arrays.asList(new SimpleGrantedAuthority("ROLE_SELLER")));
return Jwts.builder()
.setSubject(subject)
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(SignatureAlgorithm.HS512, SIGNING_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String userId = getUserIdFromToken(token);
return (
userId.equals(userDetails.getUsername())
&& !isTokenExpired(token));
}
/**
* 토큰 유효성체크, 토큰값에 userid가 존재하는지 여부만 체크한다. 만료기한 x
* @param token
* @return
*/
public Boolean validateToken(String token) {
final String userId = getUserIdFromToken(token);
return !userId.isEmpty();
}
public Boolean validateTokenForAdmin(String token) {
final String userId = getUserIdFromToken(token);
return (!userId.isEmpty() && !isTokenExpired(token));
}
}
JWT token 구현에 핵심이 되는 클래스로 위에서 설정한 Secret Key로 토큰을 검증한다.
주요 기능으로는 token을 발급하고 token에서 userId를 추출하여 token의 유효성 검사 처리를 한다.
상황에 따라 Expired 기능을 넣어 token의 만료 기한을 설정할 수 있다.
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
public static final String TOKEN_PREFIX = "Bearer";
public static final String HEADER_STRING = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String userId = null;
String auth = null;
String authToken = null;
if (header != null) {
authToken = header;
try {
userId = jwtTokenUtil.getUserIdFromToken(authToken);
auth = jwtTokenUtil.getAdminAuthFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("an error occured during getting username from token", e);
} catch (ExpiredJwtException e) {
logger.warn("the token is expired and not valid anymore", e);
} catch(SignatureException e){
logger.error("Authentication Failed. Username or Password not valid.");
}
}
if (userId != null && auth.equals("") && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtTokenUtil.validateToken(authToken)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(authToken, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + userId + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} else if(userId != null && auth.equals("AD") && SecurityContextHolder.getContext().getAuthentication() == null) {
if (jwtTokenUtil.validateTokenForAdmin(authToken)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(authToken, null, Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("authenticated user " + userId + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
JwtAuthenticationFilter는 OncePerRequestFilter를 상속받은 클래스로 요청당 한번의 filter를 수행하도록 doFilterInternal() 메서드를 구현한다.
Header에서 Authorization 값을 꺼내어 토큰을 검사하고 해당 유저가 실제 존재하는지 검사하는 등의 전반적인 인증처리를 여기서 진행한다.
@RestController
@RequestMapping("/v1/user")
public class UserController extends BaseController {
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil tokenUtil;
@RequestMapping(value="/join", method=RequestMethod.POST)
public BaseModel userJoin(@RequestBody UserModel userModel) throws Exception {
BodyModel body = new BodyModel();
Map<String, Object> user = userService.getUserTokenWithUser(userModel.getPhone_num());
body.setBody(user);
return ok(body);
}
}
@Service("userService")
public class UserService {
@Value("${aes.key}")
private String AES_KEY;
@Autowired
private UserDao userDao;
@Autowired
private JwtTokenUtil jwtTokenUtil;
public Map<String, Object> getUserTokenWithUser(String phone_num) throws Exception {
Map<String, Object> param = Maps.newHashMap();
param.put("phone_num", phone_num);
param.put("aesKey", AES_KEY);
UserModel user = userDao.getUserInfo(param);
Map<String, Object> result = Maps.newHashMap();
result.put("token", jwtTokenUtil.generateToken(user));
result.put("user_id", user.getUser_id());
result.put("user_nm", user.getUser_nm());
result.put("phone_num", user.getPhone_num());
return result;
}
}
마지막으로 Controller와 Service를 구현하여 로그인 시 실제 DB에 사용자 정보를 가져와 토큰을 생성하여 반환해준다.