티스토리 뷰
728x90
출처
토큰관리용 테이블생성
CREATE TABLE persistent_logins (
series VARCHAR(64) NOT NULL,
username VARCHAR(64) NOT NULL,
token VARCHAR(64) NOT NULL,
last_used DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY USING BTREE (series)
);
Domain 객체
package com.bx.domain;
import java.util.Date;
public class PersistentLogins {
// pk
private String series;
private String username;
private String token;
private Date last_used;
public PersistentLogins() {
}
public PersistentLogins(String username, String series, String token, Date last_used) {
this.username = username;
this.series = series;
this.token = token;
this.last_used = last_used;
}
public void setSeries(String series) {
this.series = series;
}
public String getSeries() {
return this.series;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return this.username;
}
public void setToken(String token) {
this.token = token;
}
public String getToken() {
return this.token;
}
public void setLast_used(Date last_used) {
this.last_used = last_used;
}
public Date getLast_used() {
return this.last_used;
}
}
Mapper 인터페이스
package com.bx.persistence;
import com.bx.domain.PersistentLogins;
public interface PersistentLoginsMapper {
public PersistentLogins selectUserToken(String cookieSeries);
public int insertUserToken(PersistentLogins persistentLogins);
public int updateUserToken(PersistentLogins persistentLogins);
public int deleteOneToken(String cookieValue);
}
Mapper 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="com.bx.persistence.PersistentLoginsMapper">
<!-- selectUserToken -->
<select id="selectUserToken" parameterType="java.lang.String" resultType="com.bx.domain.PersistentLogins">
select *
from persistent_logins
where series = #{series}
</select>
<!-- updateUserToken -->
<update id="updateUserToken" parameterType="com.bx.domain.PersistentLogins" statementType="PREPARED">
update persistent_logins
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username, jdbcType=VARCHAR} ,</if>
<if test="token != null">token = #{token, jdbcType=VARCHAR} ,</if>
<if test="last_used != null">last_used = #{last_used, jdbcType=TIMESTAMP} ,</if>
</trim>
where series = #{series}
</update>
<!-- insertUserToken -->
<insert id="insertUserToken" parameterType="com.bx.domain.PersistentLogins" statementType="PREPARED">
insert into persistent_logins(
<trim suffixOverrides=",">
<if test="series != null">series ,</if>
<if test="username != null">username ,</if>
<if test="token != null">token ,</if>
<if test="last_used != null">last_used ,</if>
</trim>
) values (
<trim suffixOverrides=",">
<if test="series != null">#{series, jdbcType=VARCHAR} ,</if>
<if test="username != null">#{username, jdbcType=VARCHAR} ,</if>
<if test="token != null">#{token, jdbcType=VARCHAR} ,</if>
<if test="last_used != null">#{last_used, jdbcType=TIMESTAMP} ,</if>
</trim>
)
</insert>
<!-- deleteOneToken -->
<delete id="deleteOneToken" parameterType="java.lang.String" statementType="PREPARED">
delete from persistent_logins
where series = #{cookieValue}
</delete>
</mapper>
AbstractRememberMeServices 구현
로그인시 토큰값 DB에 저장하고, 로그아웃시 토튼값 삭제 처리
package com.oxbridge.handler;
import java.security.SecureRandom;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
import org.springframework.security.web.authentication.rememberme.CookieTheftException;
import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
import com.bx.domain.PersistentLogins;
import com.bx.persistence.PersistentLoginsMapper;
public class UserLoginRememberMeService extends AbstractRememberMeServices {
private static final Logger logger = LoggerFactory.getLogger(UserLoginRememberMeService.class);
/* token값 신규 생성을 위한 랜덤 넘버 생성 객체 */
SecureRandom random;
@Autowired
private PersistentLoginsMapper mapper;
/* 생성자 */
public UserLoginRememberMeService(String key, UserDetailsService userDetailsService) {
super(key, userDetailsService);
random = new SecureRandom();
}
@Override
/* 첫 로그인 시 쿠키 발행 및 토큰정보 DB 업데이트 */
protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
// 사용자 쿠키 겁색
String cookieValue = super.extractRememberMeCookie(request);
// 사용자 쿠키가 존재할 경우 DB에서 해당 데이터를 삭제함
// 2차 인증하지 않으면 자동로그인 되지 못하는 쿠키가 남아있을 수 있음
if (cookieValue != null) {
// series 값 기준으로 해당 토큰을 DB에서 삭제
mapper.deleteOneToken(decodeCookie(cookieValue)[0]);
}
// 새로운 series, token 값 생성
String username = successfulAuthentication.getName();
String newSeriesValue = generateTokenValue();
String newTokenValue = generateTokenValue();
// 쿠키 발급 및 DB insert
try {
PersistentLogins rememberMeVO = new PersistentLogins(username, newSeriesValue, newTokenValue, new Date());
// DB insert
mapper.insertUserToken(rememberMeVO);
// 쿠키 발행
String[] rawCookieValues = new String[] { newSeriesValue, newTokenValue };
super.setCookie(rawCookieValues, getTokenValiditySeconds(), request, response);
} catch (DataAccessException e) {
e.printStackTrace();
}
/*
// 2차 인증을 위한 메일 발송
String ip = request.getHeader("X-Forwarded-For");
if (ip == null) {
ip = request.getRemoteAddr();
}
String userAgent = request.getHeader("user-agent");
boolean isSended = emailSender.sendSecondCertifyingEmail(username, newSeriesValue, newTokenValue, ip, userAgent);
if (!isSended) {
request.getSession().setAttribute("rememberMeMsg", "메일 전송에 실패했습니다. 등록된 메일 주소를 확인해주세요.");
}
*/
}
@Override
/* 자동 로그인 로직 - 쿠키 유효성 검증 및 사용자 정보 객체 리턴 */
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) throws RememberMeAuthenticationException, UsernameNotFoundException {
// 쿠키 : series, token
// 포함된 값이 2개가 아닌 경우
if (cookieTokens.length != 2) {
throw new RememberMeAuthenticationException("잘못된 쿠키");
}
String cookieSeries = cookieTokens[0];
String cookieToken = cookieTokens[1];
// DB 토큰 정보 확인
PersistentLogins rememberMeVO = mapper.selectUserToken(cookieSeries);
// DB에 정보가 없을 경우
if (rememberMeVO == null) {
throw new RememberMeAuthenticationException("존재하지 않는 series");
}
// DB에 series는 있는데 Token 값이 같지 않을 경우
if (!cookieToken.equals(rememberMeVO.getToken())) {
// DB에서 해당 데이터 삭제
mapper.deleteOneToken(cookieSeries);
throw new CookieTheftException("변조된 쿠키 발견");
}
/*
// DB에 series는 있는데 certified가 null인 경우 (메일 인증되지 않은 쿠키)
if (rememberMeVO.getCertified() == null) {
throw new RememberMeAuthenticationException("메일 인증되지 않은 쿠키");
}
*/
// 유효기간 검증
if (rememberMeVO.getLast_used().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
// DB에서 해당 데이터 삭제
mapper.deleteOneToken(cookieSeries);
throw new RememberMeAuthenticationException("유효기간 만료 쿠키");
}
// 신규 token 값으로 업데이트
String newToken = generateTokenValue();
rememberMeVO.setToken(newToken);
rememberMeVO.setLast_used(new Date());
try {
// DB에 새로운 token 값 업데이트
mapper.updateUserToken(rememberMeVO);
// 변경된 token 값으로 새로운 쿠키 발행
String[] rawCookieValues = new String[] { cookieSeries, newToken };
super.setCookie(rawCookieValues, getTokenValiditySeconds(), request, response);
} catch (DataAccessException e) {
e.printStackTrace();
throw new RememberMeAuthenticationException("새로운 token DB 업데이트 실패");
}
//
// 모두 인증됐으면 사용자 정보 객체 찾아서 반환 (예외 발생 시 super에서 처리해줌)
return getUserDetailsService().loadUserByUsername(rememberMeVO.getUsername());
}
@Override
/* 로그아웃 시 쿠키/DB 정보 삭제 */
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
logger.debug("logout-1");
// 웹에서 모든기기 로그아웃 요청한 경우
if (request.getParameter("logoutAllWeb") != null) {
/*
String username = (String) request.getParameter("logoutAllWeb");
// 권한이 있을 경우에만 로그아웃 작동 (다른 사람이 URI로 직접 요청하는 경우를 막기 위함)
if (authentication != null && authentication.getName().equals(username)) {
mapper.deleteAllUserToken(username);
}
*/
// 인증 메일에서 모든 기기 로그아웃 요청한 경우
} else if (request.getParameter("logoutAllEmail") != null) {
/*
// 디코딩 후 사용자 토큰 데이터 모두 삭제
String encodedUsername = (String) request.getParameter("logoutAllEmail");
String[] username = emailSender.decodeValues(encodedUsername);
if (username != null) {
mapper.deleteAllUserToken(username[0]);
}
*/
// 현재 기기 로그아웃 요청일 경우
// series 기준으로 DB의 데이터 삭제
} else {
// DB token 삭제 (username이 아닌 해당 series의 정보만 삭제)
String decodedCookieValue = super.extractRememberMeCookie(request);
if (decodedCookieValue != null) {
String[] cookieTokens = super.decodeCookie(decodedCookieValue);
if (cookieTokens != null && cookieTokens.length == 2) {
mapper.deleteOneToken(cookieTokens[0]);
}
}
}
// 쿠키 삭제
super.logout(request, response, authentication);
}
/* Series, Token 랜덤값으로 생성후 인코딩 */
private String generateTokenValue() {
byte[] newToken = new byte[16];
random.nextBytes(newToken);
return new String(Base64.encode(newToken));
}
}
Spring security 설정
<bean id="archimAuthDetSource" class="com.bx.handler.ArchimAuthDetSource"/>
<bean id="rememberMeFilter" class="org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter">
<property name="rememberMeServices" ref="rememberMeServices"/>
<property name="authenticationManager" ref="authenticationManager" />
</bean>
<bean id="rememberMeServices" class="com.bx.handler.UserLoginRememberMeService">
<constructor-arg value="springRocks"/>
<constructor-arg ref="tbUserService"/>
</bean>
<bean id="rememberMeAuthenticationProvider" class="org.springframework.security.authentication.RememberMeAuthenticationProvider">
<property name="key" value="springRocks"/>
</bean>
<bean id="bxAuthenticationProvider" class="com.bx.handler.BXAuthenticationProvider"/>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="bxAuthenticationProvider" /> <!-- 권한 설정할 커스텀 Bean -->
<security:authentication-provider ref="rememberMeAuthenticationProvider" />
<security:authentication-provider user-service-ref="tbUserService"/>
</security:authentication-manager>
로그인 페이지
<div class="login-content">
<form action="<c:url value='/j_spring_security_check' />" method='POST' class="margin-bottom-0">
<input type="hidden" id="user_type" name="user_type" value="01">
<div class="form-group m-b-20">
<input type="text" name='j_username' class="form-control form-control-lg inverse-mode" placeholder="아이디" required />
</div>
<div class="form-group m-b-20">
<input type="password" name='j_password' class="form-control form-control-lg inverse-mode" placeholder="암호" required />
</div>
<div class="checkbox checkbox-css m-b-20">
<input type="checkbox" id="remember_checkbox" name="_spring_security_remember_me"/>
<label for="remember_checkbox">Remember Me</label>
</div>
<div class="login-buttons">
<button type="submit" class="btn btn-success btn-block btn-lg" style="font-size:large;">로그인</button>
</div>
<!-- csrf 공격 방어를 위해 동적 생성 -->
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token }" />
</form>
</div>
댓글
300x250
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- flex
- oracle
- 튜닝쇼 2008
- Delphi
- 서울오토살롱
- Java
- KOBA
- Delphi Tip
- NDK
- Spring
- 레이싱모델 익스트림 포토 페스티벌
- 동경
- JavaScript
- ffmpeg
- BPI-M4
- Xcode
- Spring MVC
- Mac
- Linux
- MySQL
- ble
- 지스타2007
- android
- 전예희
- ubuntu
- 일본여행
- sas2009
- 송주경
- SAS
- koba2010
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
글 보관함