오늘은 먼저 기존에 사용했던 SecurityConfig를 수정하여
Student2 테이블의 정보로 로그인할때도 로그인과 로그아웃 이후의 페이지로 연결함과 동시에
세션에 사용자정보를 저장하는 기능을 사용할 수 있도록 해보겠다.
먼저 SecurityServiceImpl1
부터 생성해보도록 하겠다.
// 로그인에서 로그인버튼 => loadUserByusername으로 이메일 정보를 넘김
// student2 테이블과 연동되는 서비스
@Service
@Slf4j
@RequiredArgsConstructor
public class SecurityServiceImpl1 implements UserDetailsService {
final String format = "SecurityServiceImpl => {}";
final Student2Repository s2Repository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 이메일을 이용해서 student2 테이블에서 정보를 꺼낸 후 User타입으로 변환해서 리턴하면
// 시큐리티가 비교후에 로그인 처리를 자동으로 수행함.
log.info(format, username);
Student2 obj = s2Repository.findById(username).orElse(null);
if (obj != null) { // 이메일이 있는경우
return User.builder()
.username(obj.getEmail()) // 아이디
.password(obj.getPassword()) // 암호
.roles("STUDENT2").build(); // 권한
}
// 아이디가 없는 경우는 User로 처리 (User는 정보 null 상태)
return User.builder()
.username("_")
.password("_")
.roles("_")
.build();
}
}
UserDetailsService
클래스에서 상속을 받아 @Override
를 통해 로그인시 세션에 아이디와 암호, 권한이 저장될 수 있도록 직접 커스터마이징 해줬다.
이를 이용하여 로그인, 로그아웃을 수행하도록 SecurityConfig
의 코드를 작성해보도록 하겠다.
final SecurityServiceImpl1 student2TableService; // student2테이블과 연동되는 서비스
@Bean // 객체를 생성함. (자동으로 호출됨.)
@Order(value = 1) // 순서를 먼저 설정
public SecurityFilterChain filterChain1(HttpSecurity http) throws Exception {
log.info("SecurityConfig => {}", "start filter chain2");
// 127.0.0.1:9090/ROOT/student2/login.do
// 127.0.0.1:9090/ROOT/student2/logout.do
// 127.0.0.1:9090/ROOT/student2/loginaction.do
// 위의 주소들만 필터함.
http.antMatcher("/student2/login.do")
.antMatcher("/student2/loginaction.do")
.antMatcher("/student2/logout.do")
.authorizeRequests().anyRequest().authenticated().and();
// 로그인 처리
http.formLogin()
.loginPage("/student2/login.do")
.loginProcessingUrl("/student2/loginaction.do")
.usernameParameter("id")
.passwordParameter("password")
.defaultSuccessUrl("/student2/home.do")
.permitAll();
// 로그아웃 처리
http.logout()
.logoutUrl("/student2/logout.do")
.logoutSuccessUrl("/home.do")
.invalidateHttpSession(true)
.clearAuthentication(true)
.permitAll();
http.userDetailsService(student2TableService);
return http.build();
}
로그인을 하면 /student2/loginaction.do
를 통해 홈화면이 뜨도록 처리함과 동시에 세션에 유저정보를 저장할 수 있도록 하였고,
로그아웃 이후에도 홈화면으로 가도록 설정을 해두었다.
(컨트롤러와 html은 기존의 방식대로 반복하는관계로 생략하도록 하겠다.)
로그인을 하고나면 회원 한명당 개인의 정보를 직접 전송하게되면 보안상 문제가 발생할 수 있다.
이를 해결하고자 아이디, 이름 비밀번호등의 정보들을 '토큰'으로 한번에 관리를 하게 되며, 이 토큰에서 각 페이지별 필요한 정보들을 추출하여 사용할 수 있다.
또한 토큰의 만료시간을 지정하여 일정시간 이후에 새로 토큰정보를 갱신하도록 하여 보안성을 강화할 수도 있다.
오늘 수업에서는 JWUtil을 생성하여 두가지의 메소드 기능을 실습해보고자 한다.
package com.example.restcontroller;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
// 컨트롤러X, 서비스X, 엔티티X ...
@Component
public class JwtUtil2 {
private final String BASEKEY = "thereisnocowlevelthegatheringshowmethemoneypoweroverwhelming";
// 토큰생성하는 메소드
public String createJwt( String id, String name ) throws Exception {
// 1. header정보
Map<String, Object> headerMap = new HashMap<>();
headerMap.put("typ","JWT"); //타입
headerMap.put("alg", "HS256"); // hash알고리즘
// 2. 토큰에 포함시킬 사용자 정보들..
Map<String, Object> claimsMap = new HashMap<>();
claimsMap.put("id", id); // 아이디
claimsMap.put("name", name); // 이름
// 3. 토큰의 만료시간 ex) 2시간 => 현재시간 + 1000 * 60 * 60 * 2
Date expiredTime = new Date();
expiredTime.setTime( expiredTime.getTime() + 1000 * 60 * 60 * 8 );
// 4. 키 발행
byte[] keyBytes = DatatypeConverter.parseBase64Binary(BASEKEY);
Key signKey = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName());
// 1 ~ 4의 정보를 이용해서 토큰 생성
JwtBuilder builder = Jwts.builder()
.setHeader(headerMap)
.setClaims(claimsMap)
.setSubject("TEST")
.setExpiration(expiredTime)
.signWith(signKey, SignatureAlgorithm.HS256);
// 토큰을 String 타입으로 변환
return builder.compact();
}
// 토큰에 대해서 검증하고 데이터를 추출하는 메소드
public boolean checkJwt(String token) throws Exception{
try {
// 1. key 준비
byte[] keyBytes = DatatypeConverter.parseBase64Binary(BASEKEY);
Claims claims = Jwts.parserBuilder()
.setSigningKey(keyBytes)
.build()
.parseClaimsJws(token)
.getBody();
System.out.println( "추출한 아이디 => " + claims.get("id"));
System.out.println( "추출한 이름 => " + claims.get("name"));
return true;
}
catch(ExpiredJwtException e1) {
System.err.println("만료시간 종료" + e1.getMessage());
return false;
}
catch(JwtException e2) {
System.err.println("토큰오류" + e2.getMessage());
return false;
}
catch(Exception e) {
System.out.println("e1과 e2 오류 아닌 모든 오류" + e.getMessage());
return false;
}
}
}
이를 활용하여 Rest방식으로 로그인해보도록 하자.
final JwtUtil2 jwtUtil2; // 컴포넌트 객체 생성
final Student2Repository s2Repository; // 저장소 객체 생성
BCryptPasswordEncoder bcpe = new BCryptPasswordEncoder(); // 암호화 객체
//127.0.0.1:9090/ROOT/api/student2/login.json
@PostMapping(value="/login.json")
public Map<String, Object> loginPOST(@RequestBody Student2 student2) {
Map<String, Object> retMap = new HashMap<>();
try {
// 1. 이메일, 암호 전송 확인
log.info("{}", student2.toString());
// 2. 이메일을 이용해서 정보를 가져옴.
Student2 retStudent2 = s2Repository.findById(student2.getEmail()).orElse(null);
// 3. 실패시 전송할 데이터
retMap.put( "status", 0 );
// 4. 암호가 일치하는지 확인 => 전송된 hash되지 않은 암호와 DB에 해시된 암호 일치 확인
if( bcpe.matches( student2.getPassword(), retStudent2.getPassword()) ) {
retMap.put( "status", 200 );
retMap.put( "token", jwtUtil2.createJwt(retStudent2.getEmail(), retStudent2.getName() ) );
}
} catch (Exception e) {
e.printStackTrace();
retMap.put( "status", -1 );
retMap.put( "error", e.getMessage() );
}
return retMap;
}
postman에서 위의 json 주소를 통해 기존 정보로 로그인해서 토큰이 잘 생성되었는지 확인해보도록 하겠다.
위와같이 아이디와 비밀번호를 입력하면
토큰이 정상적으로 발행되었음을 확인할 수 있다.
회원정보 수정 기능에 토큰을 이용할 수도 있다.
// 회원탈퇴, 비번변경, 회원정보수정 ... 로그인이 되어야 되는 모든것.
// 회언정보수정 => 토큰을 주세요. 검증해서 성공하면 정보수정을 진행하는 방식.
@PostMapping(value = "/update.json")
public Map<String, Object> updatePOST(@RequestHeader(name = "token") String token) {
Map<String, Object> retMap = new HashMap<>();
try {
// 1. 토큰을 받아서 출력
log.info("{}", token);
// 2. 실패시 전달값
retMap.put("status", 0);
// 3. 토큰을 검증
if ( jwtUtil2.checkJwt(token) == true ) {
// 3, 정보를 수정함.
retMap.put("status", 200);
}
} catch (Exception e) {
e.printStackTrace();
retMap.put("status", -1);
retMap.put("error", e.getMessage());
}
return retMap;
}
postman의 Headers에서 key값을token
, Value값을 앞서 발행한 토큰값을 넣고
변경하고자 하는 계정의 아이디값과 비밀번호값을 넣으면
성공적으로 접근했음을 확인할 수 있다. 이후에 기능을 추가하여 수정하고자 하는 정보들을 입력받아 정보들을 수정할수도 있다.