본 글은 수업시간에 나왔던 내용을 정리한 글입니다.
Back-End – Spring boot
table
회원 테이블은 위와 같이 선언되어 있다는 가정.
UserVO
클라이언트 요청 데이터 및 DB Select 데이터 추상화
public class UserVO {
private String id;
private String password;
private String name;
private String role;
public UserVO() {
super();
}
public UserVO(String id, String password, String name, String role) {
super();
this.id = id;
this.password = password;
this.name = name;
this.role = role;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
@Override
public String toString() {
return "UserVO [id=" + id + ", password=" + password + ", name=" + name + ", role=" + role + "]";
}
}
JwtUserMapper
myBatis 연동을 위한 Mapper
@Mapper
public interface JwtUserMapper {
public UserVO getUser(String id);
public void insertUser(UserVO user);
}
CustomUserDetailsService
id 값으로 유저 정보를 db select 해서 JWT 에서 사용할 유저 정보(UserDetails) 로 만들어 주는 역할
이 Service 는 Controller 에서 직접 사용하지 않고 Filter 에서 token 인증할때 사용하게 된다.
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private JwtUserMapper jwtUserMapper;
@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
UserVO user = jwtUserMapper.getUser(id);
return new org.springframework.security.core.userdetails.User(user.getId(), user.getPassword(),
new ArrayList<>());
}
}
AuthService
token을 생성해주는 역할, token으로부터 정보를 추출하는 역할, token의 유효성을 검사하는 역할을 한다.
@Service
public class AuthService {
private String secret = "kkang";
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, username);
}
////token 생성을 하는 createToken
private String createToken(Map<String, Object> claims, String subject) {
//claims라고 하는건 token으로 만들고 싶은 실질적인 값을 넣는 곳이고
//expiration은 token의 유효시간 설정을 하는 부분이다.(payload 부분)
//뒤에 나오는 sign 관련은 token의 signature를 설정하는 부분이고 이곳에서는 알고리즘과 secret key가 필요하다.(signature 부분)
//이렇게 조합해서 마지막에 compact()를 수행하면 String으로 된 JWT가 생성이 된다.
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, secret).compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
JwtFilter
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private AuthService authService;
@Autowired
private CustomUserDetailsService service;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
FilterChain filterChain) throws ServletException, IOException {
//request로부터 "Authorization" 이라는 Header 값을 추출해낸다. JWT를 발급받고 그 값을 Authorization이라는
//Header에 넣어서 다시 요청을 보낸 것이다.
String token = httpServletRequest.getHeader("Authorization");
String userName = null;
//authorizationHeader에 값이 있고 이 값이 "Bearer "로 시작한다면 authorizationHeader를 통해 token을 추출해낼수 있다.
//"Bearer "가 7자이기 때문에 위와 같이 substring을 한 것이고 추출한 token 값을
//jwtUtil 이라는 JWT를 발급 또는 해석해주는 클래스를 통해 userName을 추출한다.
if (token != null) {
System.out.println("doFilterInternal.....2.......");
userName = authService.extractUsername(token);
System.out.println("JwtFilter : userName - "+userName);
}
//추출한 userName이 null이 아니라는것은 token의 값이 정상적이라는것을 나타내고,
//SecurityContextHolder (Spring Security의 필요한 값을 담는 공간)의 authentication이 비어 있다면
//이는 최초 인증이라는 뜻이므로 userName을 통해서 Spring Security Authentication에 필요한 정보를 setting 한다.
if (userName != null && SecurityContextHolder.getContext().getAuthentication() == null) {
System.out.println("doFilterInternal.....3.......");
UserDetails userDetails = service.loadUserByUsername(userName);
if (authService.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
//작업이 끝났으면 다음 필터를 수행하기 위한 Chain을 태운다.
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
AuthController
클라이언트 요청을 처리하기 위한 Controller
@Controller
public class AuthController {
@Autowired
private AuthService authService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private JwtUserMapper jwtUserMapper;
@GetMapping("/mobile/welcome")
@ResponseBody
public String welcome(@RequestParam String name) {
return "Welcome "+name;
}
@GetMapping("/mobile/security")
@ResponseBody
public String security() {
return "security page.....";
}
@PostMapping("/mobile/register.do")
@ResponseBody
public String registerUser(@RequestBody UserVO userVO) throws Exception {
System.out.println("register controller :"+userVO.getId()+","+userVO.getPassword()+","+userVO.getName()+","+userVO.getRole());
userVO.setPassword(passwordEncoder.encode(userVO.getPassword()));
System.out.println("register controller :"+userVO.getId()+","+userVO.getPassword()+","+userVO.getName()+","+userVO.getRole());
userVO.setRole("User");
jwtUserMapper.insertUser(userVO);
return "register success";
}
@PostMapping("/mobile/login.do")
@ResponseBody
public String generateToken(@RequestBody UserVO userVO) throws Exception {
System.out.println("........."+userVO.getId()+", "+userVO.getPassword());
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(userVO.getId(), userVO.getPassword()));
return authService.generateToken(userVO.getId());
}
}
SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtFilter jwtFilter;
public SecurityConfig() {
super();
}
public SecurityConfig(boolean disableDefaults) {
super(disableDefaults);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Primary
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public void configure(WebSecurity web) {
web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/mobile/welcome", "/*.html", "/favicon.ico", "/**/*.do", "/**/*.css", "/**/*.js",
"/h2-console/**");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
CharacterEncodingFilter filter = new CharacterEncodingFilter();
filter.setEncoding("UTF-8");
filter.setForceEncoding(true);
http.addFilterBefore(filter, CsrfFilter.class);
/// authenticate 로 들어오면 모두 허용, 그 외에는 인증을 거쳐야 한다는 내용이다.
// 즉 /authenticate 인 경우에 JWT 를 발급해주는 로직이 들어가야 하고
// 나머지 모든 로직은 JWT를 통해 인증이 된 경우만 사용할 수 있다고 보면 된다.
http.csrf()
.disable()
.authorizeRequests()
.antMatchers("/authenticate")
.permitAll()
.antMatchers("/register")
.permitAll().anyRequest().authenticated().and().exceptionHandling().and().sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// UsernamePasswordAuthenticationFilter를 통과하기 전에 jwtFilter 라는것을 지나도록 설정을 했다.
// Spring Security에는 많은 filter가 있고 이를 순서대로 통과하며 인증에 대한 처리를 한다.
// 인증을 하는데 있어서 jwtFilter라는 것을 추가해 request로부터 날아온 JWT를 처리하여 이에 대한 결과를 사용자 정보에 추가를
// 하기 위함이다.
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
}
Android with Retrofit
JWT 인증이 필요한 요청과 JWT 인증이 필요없는 요청 구분 테스트
로그인시 서버에서 전달하는 토큰을 SharedPreference 로 저장
인증이 필요한 경우 저장된 토큰을 Retrofit 요청시 헤더에 추가
build.gradle
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.1'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
Api Service
interface ApiService {
@GET("welcome")
fun welcome(
@Query("name") name: String
): Call<String>
@GET("security")
fun security(): Call<String>
@POST("register.do")
fun signup(
@Body user: User
): Call<String>
@POST("login.do")
fun signin(
@Body user: User
): Call<String>
}
로그인
로그인 결과로 서버에서 전달되는 토큰을 Preference 로 저장
val apiService = (applicationContext as MyApplication).apiService
val call = apiService.signin(User(id = binding.idView.text.toString(), password = binding.pwView.text.toString(), name = null))
call.enqueue(object : Callback<String>{
override fun onResponse(call: Call<String>, response: Response<String>) {
if(response.code() == 200){
val token = response.body()!!
val editor = (applicationContext as MyApplication).prefs.edit()
editor.putString("token", token)
editor.commit()
Toast.makeText(this@LoginActivity, "signin success", Toast.LENGTH_SHORT).show()
finish()
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
t.printStackTrace()
call.cancel()
}
})
Retrofit 설정
인증이 필요한 요청을 구분하여 저장된 토큰을 헤더에 지정
val authInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = prefs.getString("token", "") ?: ""
Log.d("kkang","token:$token:")
Log.d("kkang","${chain.request().url.toString()}")
val url = chain.request().url.toString()
if(url.contains("welcome") || url.contains(".do")){
return chain.proceed(chain.request())
}else {
val request = chain.request().newBuilder()
.addHeader("Authorization", "$token")
.build()
return chain.proceed(request)
}
}
}
val retrofit: Retrofit
get() {
val builder = OkHttpClient.Builder()
builder.addInterceptor(authInterceptor)
val interceptor = HttpLoggingInterceptor()
interceptor.level = HttpLoggingInterceptor.Level.BODY
builder.addInterceptor(interceptor)
val client = builder.build()
val gson = GsonBuilder()
.setLenient()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS")
.create()
return Retrofit.Builder()
.baseUrl("http://10.0.2.2:8080/mobile/")
.client(client)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
}
'Android' 카테고리의 다른 글
Progress Indicator (0) | 2022.08.01 |
---|---|
Datastore (0) | 2022.07.29 |
Multiple File upload, download with retrofit, Glide, spring boot (0) | 2022.07.25 |
Android – Spring Boot Network Programming with Retrofit (0) | 2022.07.21 |
[깡쌤의 안드로이드 프로그래밍 with 자바 - 2022 - 쌤즈] 정리 25 - GoogleMap (0) | 2022.05.09 |