회원가입, 로그인, 로그인한 유저의 정보 조회, 로그아웃 기능
Updated:
MySQL을 사용하여 회원가입 기능을 구현하기 위해서 다음과 같은 dependency를 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'org.springframework.security:spring-security-test'
runtimeOnly 'com.mysql:mysql-connector-j'
데이터베이스 테이블 설계
MySQL CREATE TABLE
먼저 회원의 정보를 저장할 데이터베이스를 생성한다. CREATE문을 통해 데이터베이스를 생성하고, USE를 통해 해당 데이터베이스를 사용한다.
CREATE DATABASE AIML;
USE AIML;
회원가입 기능을 구현해야 하므로, 다음과 같이 TABLE을 생성한다.
CREATE TABLE Member(
id VARCHAR(20) NOT NULL,
password VARCHAR(20) NOT NULL,
name VARCHAR(5) NOT NULL,
phoneNum VARCHAR(13) NOT NULL,
CONSTRAINT Member_PK PRIMARY KEY(id)
);
MEMBER Class
회원가입을 할 때, 우선 ID, Password가 필요하고 이름, 전화번호를 MyPage에 출력하는 것을 해야하므로, Member에 위 Attribute를 설정하였다. 이후, 다음과 같은 Java 코드를 작성한다.
package com.Member.aiml_server_2024.userInfo;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor // JPA를 이용하기 위해 기본 생성자를 Lombok으로 선언
@Getter
@Entity(name = "Member") // 데이터베이스 테이블과 매핑되는 클래스임을 선언
public class Member {
@Id // id 칼럼을 MEMBER 테이블의 기본키로 설정
private String id;
private String password;
private String name;
private String phoneNum;
@Builder
public Member(String id, String password, String name, String phoneNum) {
this.id = id;
this.password = password;
this.name = name;
this.phoneNum = phoneNum;
}
@Getter
@Setter
@NoArgsConstructor
public static class SaveRequest {
private String id;
private String password;
private String name;
private String phoneNum;
@Transient
public Member toEntity() {
return Member.builder()
.id(this.id)
.password(this.password)
.name(this.name)
.phoneNum(this.phoneNum)
.build();
}
}
}
DTO(Data Transfer Object) 클래스는 데이터 전송을 담당하는 클래스로, 위와같이 Member 클래스의 내부 정적 클래스로 선언했다.
application.properties
application.properties에 다음 내용을 추가한다.
## Spring JDBC 연동 정보 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url = jbcc:mysql://localhost:3306/aiml?serverTimezone=Asia/Seoul
spring.datasource.username=root
spring.datasource.password=//MySQL 비밀번호
##Spring Data JPA
#true 설정시 JPA 쿼리문 확인 가능
spring.jpa.show-sql=true
# 데이터베이스 생성 쿼리를 보기 위해 create로 설정
spring.jpa.hibernate.ddl-auto=create
Spring Security 설정
기존 API 설정
먼저 Spring Security를 사용하면 기존 Firebase의 데이터를 가져오는 API를 호출하면 401 Error가 발생하므로 다음과 같이 SecurityConfig.java를 생성해준다.
package com.Member.aiml_server_2024.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
@Configuration
@EnableWebSecurity // Spring Security 설정 활성화
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/location/**", "/UserInfo/get"));
// 예외처리 하고 싶은 url
}
}
WebSecurityCustomizer는 Spring Security 설정을 커스터마이징하기 위한 인터페이스다. 이 메서드가 수행하는 역할은 특정 URL 패턴을 Spring Security의 인증 및 권한 검사에서 예외 처리하는 것이다. 즉, 해당 경로들에 대해서는 보안 필터를 적용하지 않고 접근을 허용하게 된다.
이제 SecurityFilterChain과 PasswordEncoder 메서드를 만들어본다.
package com.Member.aiml_server_2024.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/location/**", "/UserInfo/get"));
// 예외처리 하고 싶은 url
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/").authenticated()
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/login") // 사용자 정의 로그인 페이지
.loginProcessingUrl("/api/login") // 로그인 처리 URL
.defaultSuccessUrl("/") // 로그인 성공 시 이동할 URL
)
.logout(logout -> logout
.logoutUrl("/logout") // 로그이웃 처리 URL
)
.csrf(csrf -> csrf.disable());
return httpSecurity.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
이제 메인 페이지인 “/” 페이지는 인증받은 사용자만 접근할 수 있으며, 인증받지 않은 사용자가 접근할 경우 로그인 페이지 “/login”로 이동하게 된다. 또한 사용자가 입력한 로그인 폼을 “/api/login”으로 POST method로 전송하면 loginProcessingUrl() 메서드를 통해 해당 요청이 될 시 SpringSecurity가 직접 알아서 로그인 과정을 진행해준다.
Spring
LoginController
사용자가 요청한 페이지를 반환해주는 Controller를 작성한다.
package com.Member.aiml_server_2024.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LoginController {
@GetMapping("/")
public String main() {
return "main";
}
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/register")
public String register() {
return "register"; // 회원가입 페이지
}
}
이제 인증받지 않은 사용자가 메인페이지 “/”에 접근하면 로그인 페이지를 반환하거나 회원가입 후 로그인 페이지로 접근할 수 있게 된다.
MemberApiController, MemberService, MemberRepository
package com.Member.aiml_server_2024.controller;
import com.Member.aiml_server_2024.userInfo.Member;
import com.Member.aiml_server_2024.userInfo.MemberService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MemberApiController {
private final MemberService memberService;
public MemberApiController(MemberService memberService) {
this.memberService = memberService;
}
@PostMapping("/api/member")
public void save(@RequestBody Member.SaveRequest member) {
memberService.save(member);
}
}
package com.Member.aiml_server_2024.userInfo;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.core.userdetails.User;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class MemberService implements UserDetailsService {
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
public MemberService(MemberRepository memberRepository, PasswordEncoder passwordEncoder) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
}
public void save(Member.SaveRequest member) {
member.setPassword(passwordEncoder.encode(member.getPassword()));
memberRepository.save(member.toEntity());
}
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findById(username)
.orElseThrow(() -> new UsernameNotFoundException("username"));
return toUserDetails(member);
}
private UserDetails toUserDetails(Member member) {
return User.builder()
.username(member.getId())
.password(member.getPassword())
.build();
}
}
package com.Member.aiml_server_2024.userInfo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, String> {
}
PostMan 실행 결과
Flutter
먼저 main.dart는 다음과 같이 해주었다.
import 'package:flutter/material.dart';
import 'package:login_page/HomeScreen.dart';
import 'package:login_page/MyPage.dart';
import 'package:login_page/Navigation.dart';
import 'package:login_page/SignIn.dart';
void main() async {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super (key : key);
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomeScreen(),
'/infra_info': (context) => MyPage(),
'/navigator': (context) => SignIn(),
'/message': (context) => Navigation()
},
);
}
}
Flutter에서 로그인을 구현하기 위해서 Spring의 SecurityConfig.java를 다음과 같이 수정하였다.
package com.Member.aiml_server_2024.config;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig {
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/location/**", "/UserInfo/get"));
// 예외처리 하고 싶은 url
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/infra_info").authenticated()
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/navigator") // 사용자 정의 로그인 페이지
.loginProcessingUrl("/api/login") // 로그인 처리 URL
.usernameParameter("id")
.passwordParameter("password")
// .defaultSuccessUrl("/infra_info", true) // 로그인 성공 시 이동할 URL
.successHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Login successful\"}");
response.getWriter().flush();
})
.failureHandler((request, response, exception) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Login failed\"}");
response.getWriter().flush();
})
.permitAll() // 로그인 페이지 접근 허용
)
.logout(logout -> logout
.logoutUrl("/logout") // 로그이웃 처리 URL
)
.csrf(csrf -> csrf.disable());
return httpSecurity.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
다음은 SignIn.dart이다.
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'package:login_page/MyPage.dart';
import 'package:login_page/JoinMember.dart';
class SignIn extends StatefulWidget {
@override
_SignInState createState() => _SignInState();
}
class _SignInState extends State<SignIn> {
final TextEditingController _idController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
Future<void> _signIn(String id, String password) async {
// String id = _idController.text; // 'id'로 변경
// String password = _passwordController.text;
final url = Uri.parse('http://10.0.2.2:8081/api/login');
final response = await http.post(
url,
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: {'id': id, 'password': password},
);
if(response.statusCode == 200) {
print('Login succesful');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context)=> MyPage()),
);
} else {
print('Login failed: ${response.statusCode}, ${response.body}');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('로그인'),
),
body: Padding(
padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 10.0),
TextField(
controller: _idController, // 'id'를 입력받기 위한 컨트롤러
decoration: InputDecoration(
labelText: "id",
// border: OutlineInputBorder(),
// hintText: "ID를 입력하세요", // 프롬프트 수정
),
),
SizedBox(height: 10.0),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
// border: OutlineInputBorder(),
// hintText: "비밀번호를 입력하세요",
),
obscureText: true,
),
SizedBox(height: 10.0),
ElevatedButton(
onPressed: (){
_signIn(_idController.text, _passwordController.text);
},
child: Text('Login'),
// onPressed: _signIn,
// child: Text('로그인'),
),
ElevatedButton(
onPressed: (){
Navigator.push(
context,
MaterialPageRoute(builder: (context) => JoinMember())
);
}, child: Text('회원가입'))
],
),
),
);
}
}
위 페이지에서 회원가입 버튼을 클릭하면 다음과 같은 페이지로 이동한다.
import 'package:flutter/cupertino.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:login_page/SignIn.dart';
class JoinMember extends StatefulWidget {
@override
_JoinMemberState createState() => _JoinMemberState();
}
class _JoinMemberState extends State<JoinMember> {
final TextEditingController _idController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _phoneNumController = TextEditingController();
Future<void> _Join(String id, String password, String name, String phoneNum) async {
final url = Uri.parse('http://10.0.2.2:8081/api/member');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'id': id, 'password': password, 'name': name, 'phoneNum': phoneNum}),
);
if(response.statusCode == 200) {
print('Signup succesful');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context)=> SignIn()),
);
} else {
print('Signup failed: ${response.body}');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('회원가입, 나중에 이 Text 삭제 예정'),
),
body: Padding(padding: EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
SizedBox(height: 10.0),
TextField(
controller: _idController,
decoration: InputDecoration(
labelText: "아이디"
),
),
SizedBox(height: 10.0),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: "비밀번호"
),
),
SizedBox(height: 10.0),
TextField(
controller: _nameController,
decoration: InputDecoration(
labelText: "이름"
),
),
SizedBox(height: 10.0),
TextField(
controller: _phoneNumController,
decoration: InputDecoration(
labelText: "전화번호"
),
),
SizedBox(height: 10.0),
ElevatedButton(onPressed: () {
_Join(_idController.text, _passwordController.text, _nameController.text, _phoneNumController.text);
}, child: Text('회원가입'),)
],
)),
);
}
}
이제 로그인에 성공하면 다음과 같은 페이지로 이동한다.
import 'widget/CommonScaffold.dart';
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class MyPage extends StatefulWidget{
//const Member({Key? key}) : super(key: key);
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
@override
Widget build(BuildContext context) {
return CommonScaffold(
title: Text("내 정보"),
body: SingleChildScrollView(
child: Center(
child: Container(
decoration: BoxDecoration(
//border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10.0),
color: Color(0xffD9D9D9),
),
margin: EdgeInsets.all(20.0),
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 400,
height: 55,
// margin: EdgeInsets.only(right: .0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
// padding: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(height: 5.0,),
Text(
'홍길동',
style: TextStyle(fontSize: 16.0,),
textAlign: TextAlign.right,
),
SizedBox(height: 5.0,),
Text(
'AAA @ email.com',
style: TextStyle(fontSize: 11.0, color: Colors.grey),
),
],
),
),
SizedBox(height: 20.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 첫 번째 버튼 동작
},
child: Text('개인정보수정',
style: TextStyle(fontSize: 11.0),
),
),
),
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 두 번째 버튼 동작
},
child: Text('로그아웃',
style: TextStyle(fontSize: 11.0),
),
),
),
],
),
],
),
),
),
),
);
}
}
이제 로그인 시 사용자의 정보를 가져오는 기능을 구현해본다.
사용자 정보 가져오는 기능
먼저 사용자의 이름과 전화번호를 출력하기 위해서 다음과같이 Member.java를 수정한다.
package com.Member.aiml_server_2024.userInfo;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Transient;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor // JPA를 이용하기 위해 기본 생성자를 Lombok으로 선언
@Getter
@Entity(name = "Member") // 데이터베이스 테이블과 매핑되는 클래스임을 선언
public class Member {
@Id // id 칼럼을 MEMBER 테이블의 기본키로 설정
private String id;
private String password;
private String name;
private String phoneNum;
// private MemberAuthority authority; // USER인지 ADMIN인지 구분
@Builder
public Member(String id, String password, String name, String phoneNum) {
this.id = id;
this.password = password;
this.name = name;
this.phoneNum = phoneNum;
}
public static class SafeInfo {
private String id;
private String name;
private String phoneNum;
public SafeInfo(String id, String name, String phoneNum) {
this.id = id;
this.name = name;
this.phoneNum = phoneNum;
}
public String getId() {
return id;
}
public String getName() {
return name;
}
public String getPhoneNum() {
return phoneNum;
}
}
@Getter
@Setter
@NoArgsConstructor
public static class SaveRequest {
private String id;
private String password;
private String name;
private String phoneNum;
@Transient
public Member toEntity() {
return Member.builder()
.id(this.id)
.password(this.password)
.name(this.name)
.phoneNum(this.phoneNum)
.build();
}
}
}
또한 이 과정에서 Jwt를 이용해야하므로, JwtTokenProvider.java를 생성한다.
package com.Member.aiml_server_2024.config.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Date;
@Component
public class JwtTokenProvider {
// JWT 토큰 서명에 사용할 키
private final String SECRET_KEY = Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded());
// JWT 토큰 생성 메서드
public String createdToken(String id) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 86400000); // 24 시간동안 유요한 토큰
return Jwts.builder()
.setSubject(id)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String getMemberIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
또한 SecurityConfig.java와 MemberApiController.java를 다음과 같이 수정한다.
package com.Member.aiml_server_2024.config.auth;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Date;
@Component
public class JwtTokenProvider {
// JWT 토큰 서명에 사용할 키
private final String SECRET_KEY = Base64.getEncoder().encodeToString(Keys.secretKeyFor(SignatureAlgorithm.HS256).getEncoded());
// JWT 토큰 생성 메서드
public String createdToken(String id) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + 86400000); // 24 시간동안 유요한 토큰
return Jwts.builder()
.setSubject(id)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public String getMemberIdFromToken(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
package com.Member.aiml_server_2024.controller;
import com.Member.aiml_server_2024.config.auth.JwtTokenProvider;
import com.Member.aiml_server_2024.userInfo.Member;
import com.Member.aiml_server_2024.userInfo.MemberRepository;
import com.Member.aiml_server_2024.userInfo.MemberService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
@RestController
public class MemberApiController {
private final MemberService memberService;
private final JwtTokenProvider jwtTokenProvider;
private final MemberRepository memberRepository;
public MemberApiController(MemberService memberService, JwtTokenProvider jwtTokenProvider, MemberRepository memberRepository) {
this.memberService = memberService;
this.jwtTokenProvider = jwtTokenProvider;
this.memberRepository = memberRepository;
}
@PostMapping("/api/member")
public void save(@RequestBody Member.SaveRequest member) {
memberService.save(member);
}
// @GetMapping("/api/member/info")
// public ResponseEntity<Member> getMemberInfo(Authentication authentication) {
// String id = authentication.getName();
// Member member = memberService.getUserById(id);
// return ResponseEntity.ok(member);
// }
@GetMapping("/api/member/info")
public ResponseEntity<Member.SafeInfo> getMemberInfo(@RequestHeader("Authorization") String token) {
String jwt = token.replace("Bearer ", "");
String memberId = jwtTokenProvider.getMemberIdFromToken(jwt);
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("Member not found"));
Member.SafeInfo safeInfo =
new Member.SafeInfo(member.getId(), member.getName(), member.getPhoneNum());
return ResponseEntity.ok(safeInfo);
}
}
Flutter에서 jwt 토큰을 받을 수 있는 함수를 구현한다.
import 'package:shared_preferences/shared_preferences.dart';
// JWT 토큰을 저장하는 함수
Future<void> saveJwtToken(String token) async{
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('jwtToken', token);
}
// JWT 토큰을 불러오는 함수
Future<String?> getJwtToken() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
return prefs.getString('jwtToken');
}
// JWT 토큰을 삭제하는 함수
Future<void> removeJwtToken() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('jwtToken');
}
이제 Flutter의 MyPage.dart를 다음과 같이 수정해주면 로그인한 사용자의 정보가 출력된다.
import 'dart:convert';
import 'package:login_page/token_storage.dart';
import 'widget/CommonScaffold.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class MyPage extends StatefulWidget{
//const Member({Key? key}) : super(key: key);
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String userName = '';
String userPhone = '';
@override
void initState() {
super.initState();
fetchUserInfo(); // 페이지 로드 시 사용자 정보 가져오기
}
Future<void> fetchUserInfo() async {
String? jwtToken = await getJwtToken();
if(jwtToken != null) {
final response = await http.get(
Uri.parse('http://10.0.2.2:8081/api/member/info'),
headers: {
'Authorization' : 'Bearer $jwtToken',
},
);
if(response.statusCode == 200) {
var userData = jsonDecode(response.body);
setState(() {
userName = userData['name'] ?? 'Unknown'; // name이 null이면 'Unknown'
userPhone = userData['phoneNum'] ?? 'No phone';
});
} else {
print("Failed to load user info: ${response.statusCode}, ${response.body}");
}
} else {
print("JWT Token is missing");
}
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
title: Text("내 정보"),
body: SingleChildScrollView(
child: Center(
child: Container(
decoration: BoxDecoration(
//border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10.0),
color: Color(0xffD9D9D9),
),
margin: EdgeInsets.all(20.0),
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 400,
height: 55,
// margin: EdgeInsets.only(right: .0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
// padding: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(height: 5.0,),
Text(
userName.isNotEmpty ? userName : '이름을 불러오는 중...',
style: TextStyle(fontSize: 16.0,),
textAlign: TextAlign.right,
),
SizedBox(height: 5.0,),
Text(
userPhone.isNotEmpty ? userPhone : '전화번호흫 불러오는 중...',
style: TextStyle(fontSize: 11.0, color: Colors.grey),
),
],
),
),
SizedBox(height: 20.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 첫 번째 버튼 동작
},
child: Text('개인정보수정',
style: TextStyle(fontSize: 11.0),
),
),
),
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 두 번째 버튼 동작
},
child: Text('로그아웃',
style: TextStyle(fontSize: 11.0),
),
),
),
],
),
],
),
),
),
),
);
}
}
로그아웃
이제 마지막으로 로그아웃 기능을 구현해본다.
로그아웃시 이동할 페이지를 설정해주기 위해서 SecurityConfig.java를 다음과 같이 수정한다.
package com.Member.aiml_server_2024.config;
import com.Member.aiml_server_2024.config.auth.JwtTokenProvider;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import java.util.HashMap;
import java.util.Map;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web -> web.ignoring().requestMatchers("/location/**", "/UserInfo/get"));
// 예외처리 하고 싶은 url
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/infra_info").authenticated()
.anyRequest().permitAll()
)
.formLogin(form -> form
.loginPage("/navigator") // 사용자 정의 로그인 페이지
.loginProcessingUrl("/api/login") // 로그인 처리 URL
.usernameParameter("id")
.passwordParameter("password")
// .defaultSuccessUrl("/infra_info", true) // 로그인 성공 시 이동할 URL
.successHandler((request, response, authentication) -> {
String userId = authentication.getName();
// 로그인 성공 시 JWT 토큰 생성
String token = jwtTokenProvider.createdToken(userId);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
Map<String, String> responseData = new HashMap<>();
responseData.put("token", token);
responseData.put("message", "Login successful");
ObjectMapper objectMapper = new ObjectMapper();
response.getWriter().write(objectMapper.writeValueAsString(responseData));
response.getWriter().flush();
})
.failureHandler((request, response, exception) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"message\": \"Login failed\"}");
response.getWriter().flush();
})
.permitAll() // 로그인 페이지 접근 허용
)
.logout(logout -> logout
.logoutUrl("/logout") // 로그이웃 처리 URL
.logoutSuccessUrl("/navigator")
)
.csrf(csrf -> csrf.disable());
return httpSecurity.build();
}
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
이제 Flutter의 MyPage.dart를 다음과 같이 수정해준다.
import 'dart:convert';
import 'package:login_page/token_storage.dart';
import 'widget/CommonScaffold.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
class MyPage extends StatefulWidget{
//const Member({Key? key}) : super(key: key);
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String userName = '';
String userPhone = '';
@override
void initState() {
super.initState();
fetchUserInfo(); // 페이지 로드 시 사용자 정보 가져오기
}
Future<void> fetchUserInfo() async {
String? jwtToken = await getJwtToken();
if(jwtToken != null) {
final response = await http.get(
Uri.parse('http://10.0.2.2:8081/api/member/info'),
headers: {
'Authorization' : 'Bearer $jwtToken',
},
);
if(response.statusCode == 200) {
var userData = jsonDecode(response.body);
setState(() {
userName = userData['name'] ?? 'Unknown'; // name이 null이면 'Unknown'
userPhone = userData['phoneNum'] ?? 'No phone';
});
} else {
print("Failed to load user info: ${response.statusCode}, ${response.body}");
}
} else {
print("JWT Token is missing");
}
}
Future<void> logout() async {
// JWT 토큰 가져오기
String? jwtToken = await getJwtToken();
if(jwtToken != null) {
final response = await http.post(
Uri.parse('http://10.0.2.2:8081/logout'),
headers: {
'Authorization': 'Bearer $jwtToken',
},
);
if(response.statusCode == 200) {
// 로그아웃 성공 시 JWT 토큰 삭제
await removeJwtToken();
// 로그아웃 시 로그인 페이지로 이동
Navigator.pushReplacementNamed(context, '/navigator');
} else if(response.statusCode == 302) { // 302 에러
var redirectUrl = response.headers['location'];
if(redirectUrl != null) {
print("Redirecting to: $redirectUrl");
Navigator.pushReplacementNamed(context, '/navigator');
}
} else {
print("Failed to logout: ${response.statusCode}, ${response.body}");
}
} else {
print("JWT Token is missing");
}
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
title: Text("내 정보"),
body: SingleChildScrollView(
child: Center(
child: Container(
decoration: BoxDecoration(
//border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(10.0),
color: Color(0xffD9D9D9),
),
margin: EdgeInsets.all(20.0),
padding: EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 400,
height: 55,
// margin: EdgeInsets.only(right: .0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10.0),
),
// padding: EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(height: 5.0,),
Text(
userName.isNotEmpty ? userName : '이름을 불러오는 중...',
style: TextStyle(fontSize: 16.0,),
textAlign: TextAlign.right,
),
SizedBox(height: 5.0,),
Text(
userPhone.isNotEmpty ? userPhone : '전화번호흫 불러오는 중...',
style: TextStyle(fontSize: 11.0, color: Colors.grey),
),
],
),
),
SizedBox(height: 20.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 첫 번째 버튼 동작
},
child: Text('개인정보수정',
style: TextStyle(fontSize: 11.0),
),
),
),
Container(
decoration: BoxDecoration(
//color: Colors.yellow,
),
child: TextButton(
onPressed: () {
// 두 번째 버튼 동작
logout();
},
child: Text('로그아웃',
style: TextStyle(fontSize: 11.0),
),
),
),
],
),
],
),
),
),
),
);
}
}
이제 로그아웃 버튼을 클릭하면 302에러가 발생하지만 로그아웃이 처리됨을 볼 수 있다.
댓글남기기