- 이번 포스트에서는 "Authorization Code Grant Type" 방식으로 OAuth2 Server에 접근하는 OAuth2 Client를 구현하는 방법에 대해서 설명합니다.
OAuth2 Client 동작 설명
- 사용자는 Client 서버에 로그인을 요청합니다.
- Client 서버는 Auth 서버로 로그인 요청/권한 부여를 요청합니다.
- Auth 서버는 로그인 페이지를 응답합니다.
- 로그인 페이지에서 Auth 서버로 클라이언트의 정보를 요청합니다.
- Auth 서버는 SavedRequest에 저장된 클라이언트 ID를 추출하고 클라이언트정보를 반환합니다.
- 사용자는 로그인 아이디와 암호를 입력하고 승인을 요청합니다.
- Auth 서버는 입력값이 유효하다면 권한 부여코드를 생성를 생성하고 Callback 주소로 권한부여코드를 응답합니다.
- 권한부여코드를 툥하여 Auth 서버에 토큰을 요청합니다.
- Auth 서버는 Access Token(필요시 Refresh Token)을 생성하고 반환합니다.
- 전달받은 Access Token을 통하여 Resource 서버에 사용자 정보를 요청합니다. 이 때, Token에서 username값을 추출하여 사용자 식별정보로 사용합니다.
- Resource 서버는 사용자정보를 응답합니다.
- Client 서버는 사용자정보를 기반으로 계정페이지를 생성하고 반환합니다.
본 포스트의 예제에서는 Authorization Server와 Resource Server는 별도로 구현하지 않고 하나의 프로젝트에서 구현하였습니다.
OAuth2 Server 수정
WebSecurityConfigurerAdapter 수정
Ajax기반 로그인 인증을 위하여 AuthenticationSuccessHandler, AuthenticationFailureHandler를 구현합니다. 또한, JWT를 통해 접근하는 페이지의 경우 401오류가 발생하는 문제가 있어 JwtAuthTokenFilter를 사용하는 로직을 추가합니다.
@Configuration
@EnableWebSecurity
@AllArgsConstructor
@EnableGlobalMethodSecurity(
prePostEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationHandler authenticationHandler() {
return new AuthenticationHandler();
}
@Autowired
private JwtAuthEntryPoint unauthorizedHandler;
@Bean
public JwtAuthTokenFilter authenticationJwtTokenFilter() {
return new JwtAuthTokenFilter();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/home", "/js/**", "/css/**", "/img/**", "/favicon.ico").permitAll()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.successHandler(authenticationHandler())
.failureHandler(authenticationHandler())
.and()
.logout().permitAll()
// .and() // 일반 페이지에서도 401에러가 발생하여 주석처리
// .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
// .and() // SavedRequest 값이 정상처리 안되어 주석처리
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
;
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
AuthenticationHandler 구현
Ajax 통해 인증을 처리하기 위해 Login 성공/실패 응답데이터를 Json 을 반환할 수 있도록 구현합니다. 성공시에는 추가적으로 사용자 정보를 추출하여 값을 덧붙이는 로직을 구현합니다.
public class AuthenticationHandler implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
private static final Logger log = LoggerFactory.getLogger(AuthenticationHandler.class);
@Autowired
@Qualifier("oauthUserDetailsService")
private UserDetailsService userDetailsService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
ObjectMapper om = new ObjectMapper();
String redirectUrl = getReturnUrl(request, response);
log.debug("RedirectUrl : {}", redirectUrl);
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
map.put("returnUrl", redirectUrl);
Object details = authentication.getPrincipal();
if(details instanceof String) {
map.put("name", (String) details);
map.put("username", (String) details);
UserDetails user = userDetailsService.loadUserByUsername((String) details);
if(user != null) {
details = user;
}
}
if(details instanceof OAuthUserDetails) {
OAuthUserDetails userDetails = (OAuthUserDetails) details;
map.put("name", userDetails.getName());
map.put("icon", userDetails.getIcon());
}
if(details instanceof UserDetails) {
UserDetails userDetails = (UserDetails) details;
map.put("username", userDetails.getUsername());
}
log.debug("User : {}", map);
// {"success" : true, "returnUrl" : "..."}
String jsonString = om.writeValueAsString(map);
OutputStream out = response.getOutputStream();
out.write(jsonString.getBytes());
}
/**
* 로그인 하기 전의 요청했던 URL을 알아낸다.
*
* @param request
* @param response
* @return
*/
private String getReturnUrl(HttpServletRequest request, HttpServletResponse response) {
RequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
return request.getSession().getServletContext().getContextPath();
}
return savedRequest.getRedirectUrl();
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
throws IOException, ServletException {
ObjectMapper om = new ObjectMapper();
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", false);
map.put("message", exception.getMessage());
// {"success" : false, "message" : "..."}
String jsonString = om.writeValueAsString(map);
OutputStream out = response.getOutputStream();
out.write(jsonString.getBytes());
}
}
JwtAuthTokenFilter 구현
JWT를 통해 접근하는 페이지에서 401오류를 회피하기 위한 필터입닏. 페이지 접근시 JWT 토큰을 검증하고 사용자 정보를 검증하는 필터를 아래와 같이 구현합니다.
본 필터 구현을 위하여 io.jsonwebtoken 라이브러리를 추가하였습니다.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
필터에서 Access Token의 정합성과 사용자정보를 검증합니다.
public class JwtAuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtProvider tokenProvider;
@Autowired
@Qualifier("oauthUserDetailsService")
private UserDetailsService userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwt(request);
if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
String username = tokenProvider.getUserNameFromJwtToken(jwt);
logger.error("JwtAuthTokenFilter, Username: {}", username);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Can NOT set user authentication -> Message: {}", e);
}
filterChain.doFilter(request, response);
}
private String getJwt(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.replace("Bearer ", "");
}
return null;
}
}
JwtProvider 구현
JWT 토큰을 검증하고 토큰에서 사용자 정보를 추출하는 코드를 구현합니다. loadKey() 함수에서는 공개키를 사용하였습니다. 대칭키를 사용한다면 대칭키 문자열을 읽어오는 코드로 변환하여 사용해야 합니다.
@Component
public class JwtProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class);
public PublicKey loadKey() {
PublicKey publicKey = null;
try {
Resource resource = new ClassPathResource("kr.ejsoft.oauth2.publickey.txt");
publicKey = PKIUtil.loadPublicKey(resource.getFile().getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
}
return publicKey;
}
public static String asString(Resource resource) throws IOException {
Reader reader = new InputStreamReader(resource.getInputStream(), "UTF-8");
return FileCopyUtils.copyToString(reader);
}
public String getUserNameFromJwtToken(String token) {
JwtParser parser = Jwts.parser().setSigningKey(loadKey());
Jws<Claims> claims = parser.parseClaimsJws(token);
Claims body = claims.getBody();
logger.debug("body : {}", body.entrySet());
logger.debug("body.getSubject : {}", body.getSubject());
logger.debug("body.get(user_name) : {}", body.get("user_name"));
return (String) body.get("user_name");
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(loadKey()).parseClaimsJws(authToken);
return true;
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token -> Message: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token -> Message: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token -> Message: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty -> Message: {}", e.getMessage());
}
return false;
}
}
JwtAuthEntryPoint 구현
일반 페이지에서 모두 401오류를 발생하여 사용하지는 않습니다. (웹 검색을 통하여 아래 코드를 구현하여 추가하였습니다. 테스트를 통하여 401오류의 문제가 있어 WebSecurityAdapter에서는 추가하지 않았습니다.
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint, Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
Auth Controller 구현
로그인(/login), 클라이언트정보(/auth/client), 사용자정보(/api/userinfo)를 각각 처리하는 Controller 객체입니다.
@Controller
public class AuthController {
private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
@RequestMapping(value="/login", method=RequestMethod.GET)
public ModelAndView login(HttpServletRequest request, @RequestParam Map<String, String> parameters, Map<String, ?> model,
SessionStatus sessionStatus, Principal principal1) {
ModelAndView mav = new ModelAndView();
mav.setViewName("login");
return mav;
}
@RequestMapping(value="/auth/client", method=RequestMethod.GET)
@ResponseBody
public Map<String, Object> client(HttpServletRequest request) {
HttpSession session = request.getSession();
SavedRequest savedRequest = (SavedRequest) session.getAttribute("SPRING_SECURITY_SAVED_REQUEST");
Map<String, Object> map = new HashMap<String, Object>();
String clientId = null;
if(savedRequest != null) {
String[] clientIds = savedRequest.getParameterValues("client_id");
clientId = (clientIds != null && clientIds.length > 0) ? clientIds[0]: null;
logger.debug("Clientid in savedRequest : {}", clientId); // Clientid : client
logger.debug("RedirectUrl in savedRequest : {}", savedRequest.getRedirectUrl());
}
if(clientId != null && !"".equals(clientId)) {
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
if(client != null) {
if(client instanceof OAuthClientDetails) {
OAuthClientDetails details = (OAuthClientDetails) client;
map.put("icon", details.getIcon());
map.put("name", details.getName());
map.put("desc", details.getDescription());
} else {
map.put("icon", null);
map.put("name", client.getClientId());
map.put("desc", client.getClientId());
}
String[] dbscope = OAuthUtil.toArray(client.getScope());
String[] scope = savedRequest.getParameterValues("scope");
map.put("authorization", merge(dbscope, scope));
map.put("code", "200");
map.put("message", "success");
} else {
map.put("code", "404");
map.put("message", "client not found.");
}
} else {
map.put("code", "403");
map.put("message", "client id not found.");
}
return map;
}
private String[] merge(String[] all, String[] scope) {
if(all == null || scope == null || all.length == 0 || scope.length == 0) return null;
List<String> ret = new ArrayList<String>();
for(String sco : scope) {
for(String item : all) {
if(sco.trim().equals(item.trim())) {
ret.add(sco.trim());
break;
}
}
}
return ret.toArray(new String[ret.size()]);
}
@RequestMapping(value="/api/userinfo", method=RequestMethod.POST)
@ResponseBody
public Map<String, Object> userinfo() {
// Build some dummy data to return for testing
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = auth.getAuthorities();
Object principal = auth.getPrincipal();
logger.debug("Principal : {}", principal); // Principal : admin
logger.debug("Authorities : {}", authorities); // Authorities : [user, admin]
if(auth instanceof OAuth2Authentication) {
Set<String> scops = ((OAuth2Authentication) auth).getOAuth2Request().getScope();
logger.debug("Scopes : {}", scops); // Scopes : [read_profile]
}
UserDetails details = null;
Map<String, Object> map = new HashMap<String, Object>();
if(principal instanceof String) {
map.put("name", (String) principal);
map.put("username", (String) principal);
UserDetails user = userDetailsService.loadUserByUsername((String) principal);
if(user != null) {
details = user;
}
}
if(principal instanceof OAuthUserDetails) {
details = (OAuthUserDetails) principal;
} else if(principal instanceof UserDetails) {
details = (UserDetails) principal;
}
if(details != null && details.getUsername() != null) {
if(details instanceof OAuthUserDetails) {
OAuthUserDetails userDetails = (OAuthUserDetails) details;
map.put("name", userDetails.getName());
map.put("icon", userDetails.getIcon());
}
if(details instanceof UserDetails) {
UserDetails userDetails = (UserDetails) details;
map.put("username", userDetails.getUsername());
}
logger.debug("UserInfo : {}", map);
}
return map;
}
}
OAuth2 Client 구현
기본적인 Web 기반 SpringBoot 어플리케이션을 생성합니다. 생성하는 방법은 아래 글을 참고하십시오.
Spring Boot - MyBatis & log4jdbc 설정
WebSecurityConfigurerAdapter 구현
접근 경로 및 페이지에 대한 보안설정을 정의합니다.
@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
// .anyRequest().authenticated()
.and()
.logout().permitAll()
.and()
.httpBasic()
;
}
}
HomeController 구현
홈(/), 계정페이지(/account) 를 구현하는 Controller 입니다. 세션에 저장된 값에 따라 적절하게 페이지에 값을 표시합니다.
@Controller
public class HomeController {
private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
@RequestMapping(value="/", method=RequestMethod.GET)
public ModelAndView home(HttpServletRequest request) {
logger.debug("Home : /");
User user = (User) request.getSession().getAttribute("user");
if(user != null) {
return new ModelAndView("redirect:/account");
}
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
return mav;
}
@RequestMapping(value="/account", method=RequestMethod.GET)
public ModelAndView account(HttpServletRequest request) {
User user = (User) request.getSession().getAttribute("user");
logger.debug("Account : {}", user);
if(user == null) {
return new ModelAndView("redirect:/");
}
ModelAndView mav = new ModelAndView();
mav.addObject("user", user);
mav.setViewName("account");
return mav;
}
}
index.html 구현
처리결과와 로그인 링크 화면입니다.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Home</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<span th:text="${result}"></span>
<br />
<div>
<a href="/auth/login">Login</a>
</div>
</body>
</html>
account.html 구현
사용자 계정정보 화면입니다.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Account</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<img th:src="${user.icon}" style="width:100px;height:100px;border-radius:50px;">
<br />
ID : <span th:text="${user.username}"></span><br />
Name : <span th:text="${user.name}"></span><br />
<a href="/auth/refresh">Refresh</a>
<a href="/auth/logout">Logout</a>
</body>
</html>
OAuthController 구현
OAuth 인증관련 Controller 입니다. 로그인(/auth/login), 로그인 결과 페이지(/auth/callback), 토큰갱신 요청처리(/auth/refresh)를 구현하였습니다.
토큰갱신은 테스트하지 않았습니다. 전체적인 흐름은 OAuth Server에 Refresh Token를 전달하여 새로운 Access Token을 받고 사용자 정보를 갱신합니다.
@Controller
public class OAuthController {
private static final Logger logger = LoggerFactory.getLogger(OAuthController.class);
@Value("${oauth.server}")
private String oauthServer;
@Autowired
private OAuthService oauthService;
@Autowired
private UserService userService;
@Value("${oauth.client.id}")
private String oauthClientId;
@Value("${oauth.client.secret}")
private String oauthClientSecret;
@Value("${oauth.redirect.uri}")
private String oauthRedirectUri;
@Value("${api.server}")
private String apiServer;
@RequestMapping(value="/auth/login", method=RequestMethod.GET)
public String login(HttpServletRequest request) {
String state = UUID.randomUUID().toString();
request.getSession().setAttribute("oauthState", state);
logger.debug("OAuth State : /auth/login, state : {}, Uri : {}", state, oauthRedirectUri);
StringBuilder builder = new StringBuilder();
builder.append("redirect:");
builder.append(String.format("%s/oauth/authorize", oauthServer));
builder.append("?response_type=code");
builder.append("&client_id=");
builder.append(oauthClientId);
builder.append("&redirect_uri=");
builder.append(oauthRedirectUri);
builder.append("&scope=");
builder.append("read_profile");
builder.append("&state=");
builder.append(state);
return builder.toString();
}
@RequestMapping(value="/auth/callback", method=RequestMethod.GET)
public String callback(HttpServletRequest request, @RequestParam(name="code") String code, @RequestParam(name="state") String state, ModelMap map) {
// state 체크
String oauthState = (String)request.getSession().getAttribute("oauthState");
request.getSession().removeAttribute("oauthState");
logger.debug("Check, OAuth State : After, oauthState : {}", oauthState);
logger.debug("Check, OAuth Callback : {}, state : {}", code, state);
if (oauthState == null || oauthState.equals(state) == false) {
map.put("result", "not matched state");
return "index";
}
// 코드 체크
String authorizationBasicHeader = HttpUtil.makeAuthroizationBasicHeader(oauthClientId, oauthClientSecret);
OAuthToken oauthToken = oauthService.requestAccessToken(oauthServer, authorizationBasicHeader, code, oauthRedirectUri);
if (oauthToken == null || oauthToken.getError() != null) {
map.put("result", oauthToken != null ? oauthToken.getError() : "Error");
return "index";
}
logger.debug("Check, OAuth requestAccessToken : oauthToken : {}", oauthToken);
String authorizationHeader = HttpUtil.makeAuthroizationTokenHeader("Bearer", oauthToken.getAccessToken());
OAuthUser oauthUser = userService.requestUserInfo(apiServer, authorizationHeader, oauthClientId, oauthToken.getAccessToken());
if (oauthUser.getError() != null) {
oauthToken.setError(oauthUser.getError());
return "index";
}
logger.debug("Check, OAuth requestUserInfo : oauthUser : {}", oauthUser);
if(oauthUser.getUsername() != null) {
User user = userService.loadUser(oauthUser.getUsername());
if(user == null) {
user = new User();
user.set(oauthUser);
userService.insertUser(user);
}
userService.updateToken(oauthUser.getUsername(), oauthToken.getAccessToken(), oauthToken.getRefreshToken());
request.getSession().setAttribute("user", user);
} else {
map.put("result", "User not found.");
return "index";
}
return "redirect:/account";
}
@RequestMapping(value="/auth/refresh", method=RequestMethod.GET)
public Map<String, Object> refresh(HttpServletRequest request, @RequestParam(name="scope", required=false) String scope) {
Map<String, Object> map = new HashMap<>();
User user = (User) request.getSession().getAttribute("user");
if(user == null) {
map.put("result", "User not found");
return map;
}
// 연장하기
String authorizationBasicHeader = HttpUtil.makeAuthroizationBasicHeader(oauthClientId, oauthClientSecret);
OAuthToken oauthToken = oauthService.refreshAccessToken(oauthServer, authorizationBasicHeader, user.getRefreshToken(), scope);
if (oauthToken == null || oauthToken.getError() != null) {
if(oauthToken.isRefreshTokenExpired()) {
map.put("result", oauthToken != null ? oauthToken.getErrorDescription() : "Refresh Token is expired.");
} else {
map.put("result", oauthToken != null ? oauthToken.getError() : "Error");
}
return map;
}
logger.debug("Check, OAuth requestAccessToken : oauthToken : {}", oauthToken);
// user.setAccessToken(oauthToken.getAccessToken());
// user.setRefreshToken(oauthToken.getRefreshToken());
// userService.insertUser(user);
userService.updateToken(user.getUsername(), oauthToken.getAccessToken(), oauthToken.getRefreshToken());
map.put("result", "true");
map.put("user", user);
request.getSession().setAttribute("user", user);
return map;
}
}
OAuthServiceImpl 구현
Http를 통하여 OAuth 서버에 값을 요청하는 코드입니다.
@Service("oauthService")
public class OAuthServiceImpl implements OAuthService {
private static final Logger logger = LoggerFactory.getLogger(OAuthServiceImpl.class);
@Override
public OAuthToken requestAccessToken(String oauthServer, String header, String code, String redirect) {
String reqUrl = String.format("%s/oauth/token", oauthServer);
Map<String, String> paramMap = new HashMap<>();
paramMap.put("grant_type", "authorization_code");
paramMap.put("redirect_uri", redirect);
paramMap.put("code", code);
HttpPost post = HttpUtil.buildHttpPost(reqUrl, paramMap, header);
OAuthToken token = new OAuthToken();
try {
String json = HttpUtil.executeHttp(post);
Map<String, Object> map = new HashMap<String, Object>();
map = new ObjectMapper().readValue(json, new TypeReference<Map<Object, Object>>(){});
logger.debug("Token Response : {}", map);
map.get("error");
map.get("error_description");
token = new OAuthToken();
token.setTokenType((String) map.get("token_type"));
token.setAccessToken((String) map.get("access_token"));
token.setRefreshToken((String) map.get("refresh_token"));
token.setScope((String) map.get("scope"));
token.setExpriesIn((Integer) map.get("expires_in"));
// token.setJti((String) map.get("jti"));
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return token;
}
@Override
public OAuthToken refreshAccessToken(String oauthServer, String header, String refreshToken, String scope) {
String reqUrl = String.format("%s/oauth/token", oauthServer);
Map<String, String> paramMap = new HashMap<>();
paramMap.put("grant_type", "refresh_token");
paramMap.put("scope", scope);
paramMap.put("refresh_token", refreshToken);
HttpPost post = HttpUtil.buildHttpPost(reqUrl, paramMap, header);
OAuthToken token = new OAuthToken();
try {
String json = HttpUtil.executeHttp(post);
Map<String, Object> map = new HashMap<String, Object>();
map = new ObjectMapper().readValue(json, new TypeReference<Map<Object, Object>>(){});
logger.debug("Token Response : {}", map);
map.get("error");
map.get("error_description");
token = new OAuthToken();
token.setTokenType((String) map.get("token_type"));
token.setAccessToken((String) map.get("access_token"));
token.setRefreshToken((String) map.get("refresh_token"));
token.setScope((String) map.get("scope"));
token.setExpriesIn((Integer) map.get("expires_in"));
// token.setJti((String) map.get("jti"));
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return token;
}
@Override
public void logout(String tokenId, String userName) {
}
}
UserServiceImpl 구현
Http를 통하여 Resource 서버에 사용자정보를 요청하는 로직과 데이터베이스를 통하여 사용자정보를 관리합니다.
@Service
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Autowired
private UserDao dao;
@Override
public User loadUser(String userName) {
return dao.selectUserById(userName);
}
@Override
public int insertUser(User user) {
return dao.insertUser(user);
}
@Override
public boolean updateToken(String username, String accessToken, String refreshToken) {
//
User user = dao.selectUserById(username);
user.setAccessToken(accessToken);
user.setRefreshToken(refreshToken);
dao.insertUser(user);
return true;
}
@Override
public OAuthUser requestUserInfo(String apiServer, String authorizationHeader, String clientId, String token) {
OAuthUser user = null;
try {
String reqUrl = String.format("%s/api/userinfo", apiServer);
Map<String, String> paramMap = new HashMap<>();
// paramMap.put("token", token);
// paramMap.put("clientId", clientId);
HttpPost post = HttpUtil.buildHttpPost(reqUrl, paramMap, authorizationHeader);
String json = HttpUtil.executeHttp(post);
Map<String, Object> map = new HashMap<String, Object>();
map = new ObjectMapper().readValue(json, new TypeReference<Map<Object, Object>>(){});
// "error":"invalid_token","error_description":"Access token expired"
map.get("error");
map.get("error_description");
logger.debug("UserInfo : {}", map);
user = new OAuthUser();
user.setUsername((String) map.get("username"));
user.setName((String) map.get("name"));
user.setIcon((String) map.get("icon"));
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return user;
}
}
UserDao 구현
@Repository("userDAO")
public class UserDao {
@Autowired
private SqlSessionTemplate sqlSession;
public User selectUserById(String username) {
return sqlSession.selectOne("user.selectUserById", username);
}
public int insertUser(User user) {
return sqlSession.insert("user.insertUser", user);
}
public int updateUser(User user) {
return sqlSession.insert("user.updateUser", user);
}
public int deleteUser(String username) {
return sqlSession.delete("user.deleteUser", username);
}
}
user_sql.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="user">
<select id="selectUserById" resultType="kr.ejsoft.oauth2.client.model.User">
<![CDATA[
SELECT
*
FROM
user
WHERE
username = #{username}
]]>
</select>
<insert id="insertUser" keyProperty="username">
REPLACE INTO user
(username, name, icon, access_token, refresh_token)
VALUES
(#{username}, #{name}, #{icon}, #{accessToken}, #{refreshToken})
</insert>
<update id="updateUser" keyProperty="username">
UPDATE user SET
name = #{name},
icon = #{icon},
access_token = #{accessToken},
refresh_token = #{refreshToken}
WHERE
username = #{username}
</update>
<delete id="deleteUser">
DELETE FROM user
WHERE username = #{username}
</delete>
</mapper>
User 구현
public class User {
private String username;
private String name;
private String icon;
private String accessToken;
private String refreshToken;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public void set(OAuthUser oauthUser) {
this.username = oauthUser.getUsername();
this.name = oauthUser.getName();
this.icon = oauthUser.getIcon();
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("User [username=");
builder.append(username);
builder.append(", name=");
builder.append(name);
builder.append(", icon=");
builder.append(icon);
builder.append(", accessToken=");
builder.append(accessToken);
builder.append(", refreshToken=");
builder.append(refreshToken);
builder.append("]");
return builder.toString();
}
}
동작 확인
로그인 화면
계정정보 화면
글을 마치며
개략적으로 Authorization Code Grant Type 기반 클라이언트를 구현하여 보았습니다. 세부적인 모델 객체는 본 포스트에서는 적시하지 않았습니다. 아래의 소스코드를 다운로드 받아서 참고하십시오.
지금까지 10개의 포스트를 통하여 Spring Security의 OAuth2 서버와 클라이언트 구현을 해봤습니다. 지금까지의 구현한 프로젝트는 아직 실제업무에 사용하려면 다양한 테스트와 예외처리 및 필요에 따른 추가 로직을 덧붙여야 합니다. 지금까지의 포스트를 통하여 OAuth2 시스템을 약간이나마 이해하고 업무에 조금이나마 활용하셨으면 좋겠습니다. 감사합니다.
참고사이트
- Spring security with JWT always returns 401 unauthorized
https://stackoverflow.com/questions/52028457/spring-security-with-jwt-always-returns-401-unauthorized - Spring Boot bearer token authentication giving 401
https://stackoverflow.com/questions/52797019/spring-boot-bearer-token-authentication-giving-401 - Spring Security JWT REST API returning 401
https://stackoverflow.com/questions/50551219/spring-security-jwt-rest-api-returning-401
소스코드
서버의 소스코드는 여기에서, 클라이언트의 소스코드는 여기에서 다운로드 가능합니다.
'Development > Spring Framework, Security, OAuth2' 카테고리의 다른 글
OAuth Interceptor & Custom Annotation (0) | 2020.02.17 |
---|---|
Spring Framework, Interceptor와 Filter 사용하기 (0) | 2020.02.17 |
Spring Boot with OAuth2 - 9. OAuth2 Server 로그인화면 수정 (0) | 2019.12.17 |
Spring Boot with OAuth2 - 8. OAuth2 Server - Client, User Cache (0) | 2019.12.03 |
Spring Boot with OAuth2 - 7. JWT Refresh Token 관리 (0) | 2019.12.02 |