本篇作為SpringBoot2.1版本的個人開發框架 子章節,請先閱讀SpringBoot2.1版本的個人開發框架再次閱讀本篇文章
後端專案地址:SpringBoot2.1版本的個人應用開發框架
前端專案地址:ywh-vue-admin
感謝PanJiaChen大神給我們建立了這麼好的vue後端管理模板,大神有一系列的教程,在預覽地址中有系列文章的地址,還有專案github的地址,感覺大神就是帥氣。
-
vue-element-admin預覽地址:vue-element-admin
-
vue官方文件:vue官方文件
-
node安裝:node.js 安裝與環境變數配置
下載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(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的攔截器鏈中去了,這就使他們倆有了合作的關係,接下來就是建立我們的service和controller,實現最基本的登陸和退出,使用者登陸後返回一個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登陸則顯示使用者名稱或密碼錯誤