티스토리 뷰

클라이언트 사이드 랜더링을 하면 기존 서버사이드에서 사용하던 로그인 형태가 아니라 어떻게 해야할지 의문이었다.

 

열심히 검색해보고 시도하다보니 기본적인 로그인 구현은 된 것 같아 정리하려 한다.

(물론 토큰 방식으로 추후 업데이트가 필요하다.)

 

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
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/04   »
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
글 보관함