티스토리 뷰
클라이언트 사이드 랜더링을 하면 기존 서버사이드에서 사용하던 로그인 형태가 아니라 어떻게 해야할지 의문이었다.
열심히 검색해보고 시도하다보니 기본적인 로그인 구현은 된 것 같아 정리하려 한다.
(물론 토큰 방식으로 추후 업데이트가 필요하다.)
1. LoginView.vue 작성
- v-if문을 통해 로그인 상태 별 창을 다르게 뿌려준다.
- @submit.prevent를 통해 실제 폼 제출을 막고 method를 실행시켜준다.
- this.$store.dispatch()을 통해 vuex의 store 저장소에 action에 접근하여 login function을 실행할 수 있다.
<template>
<main class="form-signin w-100 m-auto">
<div v-if="loginError">
<h5>
<span class="badge text-bg-danger">
아이디 비밀번호를 확인해주세요.
</span>
</h5>
<form @submit.prevent="login()">
<div class="form-floating">
<input v-model="user" type="text" class="form-control" placeholder="ID">
<label for="floatingInput">ID</label>
</div>
<div class="form-floating">
<input v-model="password" type="password" class="form-control" id="password" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<button class="w-100 btn btn-lg btn-primary" variant="success" type="submit">Sign in</button>
</form>
</div>
<div v-else>
<h1 class="h3 mb-3 fw-normal">
<span class="badge text-bg-primary">
로그인해주세요
</span>
</h1>
<h5>로그인 하지 않았습니다.</h5>
<form @submit.prevent="login()">
<div class="form-floating">
<input v-model="user" type="text" class="form-control" placeholder="ID">
<label for="floatingInput">ID</label>
</div>
<div class="form-floating">
<input v-model="password" type="password" class="form-control" id="password" placeholder="Password">
<label for="floatingPassword">Password</label>
</div>
<button class="w-100 btn btn-lg btn-primary" variant="success" type="submit">Sign in</button>
</form>
</div>
</main>
</template>
<script>
export default {
name: 'LoginView',
data () {
return {
loginSuccess: false,
loginError: false,
user: '',
password: '',
error: false
}
},
methods: {
async login () {
try {
await this.$store.dispatch('login', {
user: this.user,
password: this.password
})
await this.$router.push({ name: 'AboutView' })
} catch (err) {
this.loginError = true
this.error = true
throw new Error(err)
}
}
}
}
</script>
<style scoped>
</style>
2. Store > index.js 작성
- store 사용을 위해 vuex를 설치하고 설정을 작성해준다.
- 서버와 통신을 하기 위해 axios도 설치 후 import.
- 상태값 등을 브라우저에서 새로고침해도 저장시켜두기 위해 vuex-persistedstate도 설치 후 import.
마지막 plugins 쪽에서 어디에 저장할지 무엇을 저장할지 설정해줄 수 있음. (좀 더 공부 필요.)
안에 비우고 createPersistedState()만 작성하면 전체가 저장된다. (필요치 않은 정보들이 저장되어 좋지 않음.)
- LoginView에서 메서드에서 불러왔던 login() action이 여기 작성된다.
프록시 설정해두면 서버(Spring)쪽에 api가 실행되고 결과값이 리턴된다.
import { createStore } from 'vuex'
import axios from 'axios'
import createPersistedState from 'vuex-persistedstate'
export default createStore({
state: {
loginSuccess: false,
loginError: false,
userName: null,
password: null
},
mutations: {
loginSuccess (state, { user, password }) {
state.loginSuccess = true
state.userName = user
state.password = password
},
loginError (state, { user, password }) {
state.loginError = true
state.userName = user
state.password = password
},
logout (state) {
state.loginSuccess = false
state.loginError = false
state.userName = null
state.password = null
}
},
actions: {
async login ({ commit }, { user, password }) {
try {
const result = await axios.get('/api/login', {
auth: {
username: user,
password: password
}
})
if (result.status === 200) {
commit('loginSuccess', {
userName: user,
userPass: password
})
}
} catch (err) {
commit('loginError', {
userName: user,
userPass: password
})
throw new Error(err)
}
},
logout ({ commit }) {
commit('logout')
}
},
getters: {
isLoggedIn: state => state.loginSuccess,
hasLoginErrored: state => state.loginError,
getUserName: state => state.userName,
getUserPass: state => state.userPass
},
modules: {
},
plugins: [
createPersistedState({
storage: window.sessionStorage
})
]
})
3. router > index.js 작성
- beforeEach를 통해 로그인이 되지 않은 상태에서 접근하려하면 로그인 창으로 보냄.
- :catchAll(.*)을 통해 지정되지 않은 경로로 접근하려 하면 리다이렉트 주소로 돌려보냄.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import LoginView from '@/views/LoginView'
import RegisterView from '@/views/RegisterView'
import AboutView from '../views/AboutView.vue'
import store from '../store'
const routes = [
{
path: '/login',
component: LoginView
},
{
path: '/Register',
component: RegisterView
},
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/protected',
name: 'AboutView',
component: AboutView,
meta: {
requiresAuth: true
}
},
// otherwise redirect to home
{ path: '/:catchAll(.*)', redirect: '/' }
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!store.getters.isLoggedIn) {
next({
path: '/login'
})
} else {
next()
}
} else {
next()
}
})
export default router
4. Logout 작성
- component로 구성해서 네비게이션으로 붙이고 있음.
login을 안한 상태에서는 login버튼이 보이고 login 한 상태에서는 logout 버튼이 보이게 작성.
- logout 누르면 store에 저장된 action이 실행되어 store에 저장된 값들이 초기화 되고 logout 됨.
그 후 로그인 페이지로 라우팅 시킴.
<template>
<header class="p-3 text-bg-dark">
<div class="container">
<div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
<ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
<li><router-link to="/" class="nav-link px-2 text-secondary">Home</router-link></li>
</ul>
<div class="text-end">
<router-link v-if="!this.$store.state.loginSuccess" to="/login" class="btn btn-outline-light me-2">Login</router-link>
<button v-if="this.$store.state.loginSuccess" @click="logout()" class="btn btn-outline-light me-2">Logout</button>
<router-link to="/register" class="btn btn-outline-light me-2">Register</router-link>
</div>
</div>
</div>
</header>
</template>
<script>
import router from '@/router/index.js'
export default {
name: 'NavComponent',
methods: {
logout () {
this.$store.dispatch('logout')
router.push('/login')
}
}
}
</script>
<style scoped>
</style>
5. Spring security 작성
- DB id, pw 매칭확인으로 작성했음. (추후 JWT 방식으로 바꿔야 함.)
- AuthenticationManagerBuilder > userDetailsServiceImpl > loadUserByUsername()을 통해 인증절차 진행 됨.
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private LoginFailHandler loginFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/api/hello").permitAll()
.antMatchers("/api/login").authenticated()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin()
.failureHandler(null);
}
//PasswordEncoder
@Bean
public BCryptPasswordEncoder encoderPwd() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(encoderPwd());
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String accountId) throws UsernameNotFoundException {
System.out.println("accountId :: " + accountId);
//여기서 받은 유저 패스워드와 비교하여 로그인 인증
Optional<Account> account = accountRepository.findByAccountId(accountId);
System.out.println(account.toString());
if (account.get() == null){
throw new UsernameNotFoundException("User not authorized.");
}
return new SecurityAccount(account.get());
}
}
public class SecurityAccount extends User {
private static final long serialVersionUID = 1L;
public SecurityAccount(Account account) {
super(account.getAccountId(), account.getPassword(), makeGrantedAuthority(account));
}
private static List<GrantedAuthority> makeGrantedAuthority(Account account){
List<GrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority(account.getRole()));
return list;
}
}
추후 회원가입, JWT 토큰 전환 작성 후 사이드 프로젝트 적용할 예정이다.
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
로컬로 테스트하려니 CORS 위배 에러가 자꾸 떠서 Webconfig 작성해줬음.
이때도 프로토콜, url, port가 일치해줘야 하는데 localhost, 127.0.0.1 url 부분을 잘맞춰서 작성해줘야 해당에러가 안뜸.
(vue.config.js 와 Webconfig java쪽을 맞춰줘야 함)
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
outputDir: "../src/main/resources/static", // 빌드 타겟 디렉토리
devServer: {
proxy: {
'/api': {
// '/api' 로 들어오면 포트 80(스프링 서버)로 보낸다
target: 'http://127.0.0.1:80',
changeOrigin: true // cross origin 허용
}
}
}
})
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:8080") // localhost url은 cors 위배
.allowedMethods("GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS")
.maxAge(3000);
}
}
'Javascript > Vue' 카테고리의 다른 글
04. Vue - Axios (0) | 2022.09.26 |
---|---|
03. Vue - 기초 정리 (0) | 2022.09.25 |
02. Vue - router (0) | 2022.09.25 |
01. Vue 3 - create package 설정 (1) | 2022.09.25 |