SpringBoot2.1版本的個人應用開發框架 - 整合vue實現前後端分離

人生長恨水發表於2019-04-11

本篇作為SpringBoot2.1版本的個人開發框架 子章節,請先閱讀SpringBoot2.1版本的個人開發框架再次閱讀本篇文章

專案地址:SpringBoot2.1版本的個人應用開發框架

感謝PanJiaChen大神給我們建立了這麼好的vue後端管理模板,大神有一系列的教程,在預覽地址中有系列文章的地址,還有專案github的地址,感覺大神就是帥氣。

下載PanJiaChen大神的vue-admin-template,這個是大神推薦的二次開發的模板,vue-element-admin大神希望是一個整合方案,當我們需要什麼再去拿什麼。

這裡我不對專案結構做介紹,在大神的系列文章中都有介紹,當我們把專案下載好以後,我們嘗試的在本地跑起來,確定沒有錯誤以後我們再進行下一步。

啟動初始vue-admin-template專案

在下載好的專案中執行cmd,先下載專案所需要的依賴後再啟動

npm install
。。。。
npm run dev
複製程式碼

效果圖,現在登陸的還是預設的使用者,我們要實現的功能是:前端與後端做互動,並在資料庫中查詢使用者時候否是有許可權登陸。

在這裡插入圖片描述

跑起來後登陸的介面如上圖,沒有預覽的功能多,所以以後我們按照我們自己想要的需求一一加進去。

後端專案

我們想要後端與前端互動起來,其實還是需要修改挺多地方的,這裡先介紹修改後端,在前後端分離的專案中多數用Token來做請求的認證,我也是實現了jwt和SpringSecurity來保護API,他們倆在我理解來看是沒有直接關係的,而是合作的關係,由SpringSecurity來決定什麼請求可以訪問我們伺服器,可以訪問的請求再由jwt來判斷是否攜帶Token,沒有攜帶的不予通過,再加上網上很多都是通過這種模式來實現的,參考的資料也比較多。

推薦: 重拾後端之Spring Boot(四):使用JWT和Spring Security保護REST API

jwt的建立

在security模組中的application-security.yml檔案中新增以下內容,jwt的加密字串是一個提前寫好的,這裡就相當於配置了三個常量,並沒有什麼特別的,之後會在類中載入,如果閒麻煩,可以直接在類中定義常量即可。

jwt:
  header: token   #jwt的請求頭
  secret: eyJleHAiOjE1NDMyMDUyODUsInN1YiI6ImFkbWluIiwiY3Jl   #jwt的加密字串
  expiration: 3600000   #jwt token有效時間(毫秒)一個小時
複製程式碼

在ywh-starter-security模組的utils包中建立JwtTokenUtil工具類,如果想看詳細的程式碼,可以前往我的GitHub檢視詳細程式碼。

package com.ywh.security.utils;

/**
 * CreateTime: 2019-01-22 10:27
 * ClassName: JwtTokenUtil
 * Package: com.ywh.security.utils
 * Describe:
 * jwt的工具類
 *
 * @author YWH
 */
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtTokenUtil {

    private String secret;

    private Long expiration;

    private String header;


    /**
     * 從資料宣告生成令牌
     *
     * @param claims 資料宣告
     * @return 令牌
     */
    private String generateToken(Map<String, Object> claims) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration);
        return Jwts.builder()
                .setClaims(claims)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
    }

    /**
     * 生成令牌
     * @return 令牌
     */
    public String generateToken(String userName) {
        Map<String, Object> claims = new HashMap<>(2);
        claims.put("sub", userName);
        claims.put("created", new Date());
        return generateToken(claims);
    }
	。。。。。。。。。。。。中間省略了程式碼

}

複製程式碼

在我們前端向後端請求時,我們要每一次的判斷是否攜帶了token,這個任務我們就交給攔截器來執行,建立JwtAuthenticationTokenFilter攔截器

package com.ywh.security.filter;

/**
 * CreateTime: 2019-01-29 18:15
 * ClassName: JwtAuthenticationTokenFilter
 * Package: com.ywh.security.filter
 * Describe:
 * spring的攔截器
 *
 * @author YWH
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    private final static Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);

    private JwtTokenUtil jwtTokenUtil;
    private UserDetailsService userDetailsService;

    @Autowired
    public JwtAuthenticationTokenFilter(JwtTokenUtil jwtTokenUtil, UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
        this.jwtTokenUtil = jwtTokenUtil;
    }

    /**
     * 該攔截器主要的功能是,攔截請求後,判斷是否攜帶token,如果未攜帶token則不予通過。
     * @param httpServletRequest http請求
     * @param httpServletResponse http響應
     * @param filterChain 攔截器
     * @throws ServletException 異常資訊
     * @throws IOException 異常資訊
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // 獲取request中jwt token
        String authHeader = httpServletRequest.getHeader(jwtTokenUtil.getHeader());
        // 驗證token是否存在
        if(authHeader != null && StringUtils.isNotEmpty(authHeader)){
            //根據token獲取使用者名稱
            String userName = jwtTokenUtil.getUsernameFromToken(authHeader);
            if(userName != null && SecurityContextHolder.getContext().getAuthentication() == null){
                // 通過使用者名稱 獲取使用者的資訊
                UserDetails userDetails = userDetailsService.loadUserByUsername(userName);
                // 驗證token和使用者資訊是否匹配
                if(jwtTokenUtil.validateToken(authHeader,userDetails)){
                    // 然後構造UsernamePasswordAuthenticationToken物件
                    // 最後繫結到當前request中,在後面的請求中就可以獲取使用者資訊
                    UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                    authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                }
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);

    }
}

複製程式碼

攔截器寫好以後,我們需要修改SecurityConfigurer類中configure(HttpSecurity httpSecurity)方法,這個類在我上兩篇文章中都有介紹,以下程式碼中我寫了跨域請求的後端實現。後面我們就不用在前端實現跨域請求的設定了,不過我也會把前端如何實現跨域寫出來的。

	/**
     * 配置如何通過攔截器保護我們的請求,哪些能通過哪些不能通過,允許對特定的http請求基於安全考慮進行配置
     * @param httpSecurity http
     * @throws Exception 異常
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                // 暫時禁用csrc否則無法提交
                .csrf().disable()
                // session管理
                .sessionManagement()
                // 我們使用SessionCreationPolicy.STATELESS無狀態的Session機制(即Spring不使用HTTPSession),對於所有的請求都做許可權校驗,
                // 這樣Spring Security的攔截器會判斷所有請求的Header上有沒有”X-Auth-Token”。
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // 設定最多一個使用者登入,如果第二個使用者登陸則第一使用者被踢出,並跳轉到登陸頁面
                .maximumSessions(1).expiredUrl("/login.html");
        httpSecurity
                // 開始認證
                .authorizeRequests()
                // 對靜態檔案和登陸頁面放行
                .antMatchers("/static/**").permitAll()
                .antMatchers("/auth/**").permitAll()
                .antMatchers("/login.html").permitAll()
                // 其他請求需要認證登陸
                .anyRequest().authenticated();

        // 注入我們剛才寫好的 jwt過濾器,新增在UsernamePasswordAuthenticationFilter過濾器之前
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 這塊是配置跨域請求的
         ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        // 讓Spring security放行所有preflight request
        registry.requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
 }
 
 /**
     * 這塊是配置跨域請求的
     * @return Cors過濾器
     */
    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration cors = new CorsConfiguration();
        cors.setAllowCredentials(true);
        cors.addAllowedOrigin("*");
        cors.addAllowedHeader("*");
        cors.addAllowedMethod("*");
        urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", cors);
        return new CorsFilter(urlBasedCorsConfigurationSource);
    }
複製程式碼

可以看到上面程式碼中,我把我們實現的攔截器放到了SpringSecurity的攔截器鏈中去了,這就使他們倆有了合作的關係,接下來就是建立我們的servicecontroller,實現最基本的登陸和退出,使用者登陸後返回一個Token,前端存在本地快取(localStorage)或者sessionStorage中,以供之後的請求使用。

package com.ywh.security.service.impl;

/**
 * CreateTime: 2019-01-25
 * ClassName: SysUserServiceImpl
 * Package: com.ywh.security.service.impl
 * Describe:
 *  業務邏輯介面的實現類
 * @author YWH
 */
@Service
public class SysUserServiceImpl extends BaseServiceImpl<SysUserDao, SysUserEntity> implements SysUserService {

    private static final Logger log = LoggerFactory.getLogger(SysUserServiceImpl.class);

    @Autowired
    private SysUserDao dao;

    @Autowired
    private AuthenticationManager authenticate;

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    /**
     * 獲取使用者詳細資訊
     * @param username 使用者名稱
     * @return 實體類
     */
    @Override
    public SysUserEntity findUserInfo(String username) {
        return dao.selectByUserName(username);
    }

    /**
     * 使用者登陸
     * @param username 使用者名稱
     * @param password 密碼
     * @return 登陸成功 返回token
     */
    @Override
    public String login(String username, String password) throws AuthenticationException {
        // 內部登入請求
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        // 驗證是否有許可權
        Authentication auth = authenticate.authenticate(authRequest);
        log.debug("===============許可權============" + auth);
        SecurityContextHolder.getContext().setAuthentication(auth);

        return jwtTokenUtil.generateToken(username);
    }
}

複製程式碼

Controller的實現,因為都是最簡單的實現,可以根據自己的需求修改,後期也可以再根據自己的想法加相應的實現即可。

package com.ywh.security.controller;

/**
 * CreateTime: 2019-01-28 16:06
 * ClassName: AuthController
 * Package: com.ywh.security.controller
 * Describe:
 * 許可權控制器
 *
 * @author YWH
 */
@RestController
@RequestMapping("auth")
public class AuthController {


    private static final Logger LOG = LoggerFactory.getLogger(AuthController.class);


    @Autowired
    private SysUserService sysUserService;


    /**
     * 登陸
     * @param map 接收體
     * @return 返回token
     */
    @PostMapping("login")
    public Result login(@RequestBody Map<String, String> map){
        try {
            String token = sysUserService.login(map.get("username"), map.get("password"));
            return Result.successJson(token);
        }catch (AuthenticationException ex){
            LOG.error("登陸失敗",ex);
            return Result.errorJson(BaseEnum.PASSWORD_ERROR.getMsg(),BaseEnum.PASSWORD_ERROR.getIndex());
        }

    }

    /**
     * 使用者詳情
     * @return 使用者詳細資訊
     */
    @Cacheable(value = "userInfo")
    @GetMapping("userInfo")
    public Result userInfo(){
        Object authentication = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        if(authentication instanceof SecurityUserDetails){
            return Result.successJson(sysUserService.findUserInfo(((SecurityUserDetails) authentication).getUsername()));
        }
        return Result.errorJson(BaseEnum.LOGIN_AGIN.getMsg(),BaseEnum.LOGIN_AGIN.getIndex());
    }

    @PostMapping("logOut")
    public Result logOut(){
        return Result.successJson("退出成功,因為token本身是無狀態,如果通過redis來控制token的生存週期,則變成了有狀態,所以暫時沒有好的解決辦法。");
    }

}

複製程式碼

到此就結束了後端專案的修改,我覺的最重要的是要明白它們是怎麼樣的工作流程,知道流程後我們就好理解很多,一步一步往下寫就可以了,碰到不會的多Google多百度即可。

jwt和SpringSecurity的流程總結:

  • 先要有一個可以生成Token的工具類。
  • 實現jwt的攔截器,判斷每一次請求是否攜帶Token。
  • 修改Security的配置類,使jwt和Security聯絡起來,有合作關係。
  • 建立service實現最簡單的登陸以及查詢使用者等操作。
  • 建立Controller,提供前端所要的介面。

前端專案

在上面我們把前端專案vue-elment-template跑起來後,需要修改挺多地方的,比較雜,我也是遇到一個錯誤修改一個地方。

在後端專案中我已經實現了後端跨域的方法,但是前端也是可以實現跨域請求的,兩者選擇哪個都可以。

  • vue-element-template跨域問題,需要把src/utils/request.js中的baseURL的地址去掉,然後在配置/config/index.js中的proxyTable 解決跨域問題。
proxyTable: {
  '/core': {
    target: 'http://192.168.0.117:8082', // 介面的域名
    // secure: false,  // 如果是https介面,需要配置這個引數
    changeOrigin: true, // 如果介面跨域,需要進行這個引數配置
    pathRewrite: {
      '^/core': '/core'
    }
  }
},
複製程式碼
  • config\dev.env.js和config\prod.env.js 修改訪問根路徑
'use strict'
module.exports = {
  NODE_ENV: '"production"',
  BASE_API: '"http://localhost:8082/core/"',
}
複製程式碼
  • src\api\login.js 解決訪問路徑問題,由於我們controller中的路徑使auth/**,所以要修改成我們自己的路徑,我只寫一個示例,剩下的按著修改。
export function login(username, password) {
  return request({
    url: '/auth/login',
    method: 'post',
    data: {
      username,
      password
    }
  })
}
複製程式碼
  • src\utils\request.js 修改token名字,這個就是修改Header頭中攜帶的Token名字,這個是後端決定的,我們在前面的yml檔案中定義的什麼這裡就寫什麼,還有就是狀態碼等。

在這裡插入圖片描述

在這裡插入圖片描述

  • src\store\modules\user.js 修改登陸等問題,在這裡我們要修改的比較多一點,語言描述也不太好描述,我就簡略劫了兩張圖,如果遇到錯誤自己解決掉正好多熟悉一下。

在這裡插入圖片描述

在這裡插入圖片描述

以上差不多就是我在前端遇到的大問題,很有很多小問題,就不一一貼了,設定了以上後,可以先試一試能不能跑起來,如果不行,可以對比我在GitHub的程式碼。

效果圖

當我們再次登陸時,可以看到我們已經是在資料庫中查詢使用者資訊並且登陸了。

使用者登入成功

如果再以預設的admin登陸則顯示使用者名稱或密碼錯誤

使用者登入失敗

相關文章