本篇做爲SpringBoot2.1版本的我的開發框架 子章節,請先閱讀SpringBoot2.1版本的我的開發框架再次閱讀本篇文章html
後端項目地址:SpringBoot2.1版本的我的應用開發框架前端
前端項目地址:ywh-vue-adminvue
感謝PanJiaChen大神給咱們建立了這麼好的vue後端管理模板,大神有一系列的教程,在預覽地址中有系列文章的地址,還有項目github的地址,感受大神就是帥氣。java
vue-element-admin預覽地址:vue-element-adminnode
vue官方文檔:vue官方文檔git
node安裝:node.js 安裝與環境變量配置github
下載PanJiaChen大神的vue-admin-template,這個是大神推薦的二次開發的模板,vue-element-admin大神但願是一個集成方案,當咱們須要什麼再去拿什麼。redis
這裏我不對項目結構作介紹,在大神的系列文章中都有介紹,當咱們把項目下載好之後,咱們嘗試的在本地跑起來,肯定沒有錯誤之後咱們再進行下一步。spring
在下載好的項目中運行cmd,先下載項目所須要的依賴後再啓動數據庫
npm install
。。。。
npm run dev
複製代碼
效果圖,如今登錄的仍是默認的用戶,咱們要實現的功能是:前端與後端作交互,並在數據庫中查詢用戶時候否是有權限登錄。
跑起來後登錄的界面如上圖,沒有預覽的功能多,因此之後咱們按照咱們本身想要的需求一一加進去。
咱們想要後端與前端交互起來,其實仍是須要修改挺多地方的,這裏先介紹修改後端,在先後端分離的項目中多數用Token來作請求的認證,我也是實現了jwt和SpringSecurity來保護API,他們倆在我理解來看是沒有直接關係的,而是合做的關係,由SpringSecurity來決定什麼請求能夠訪問咱們服務器,能夠訪問的請求再由jwt來判斷是否攜帶Token,沒有攜帶的不予經過,再加上網上不少都是經過這種模式來實現的,參考的資料也比較多。
推薦: 重拾後端之Spring Boot(四):使用JWT和Spring Security保護REST API
在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的流程總結:
在上面咱們把前端項目vue-elment-template跑起來後,須要修改挺多地方的,比較雜,我也是遇到一個錯誤修改一個地方。
在後端項目中我已經實現了後端跨域的方法,可是前端也是能夠實現跨域請求的,二者選擇哪一個均可以。
proxyTable: {
'/core': {
target: 'http://192.168.0.117:8082', // 接口的域名
// secure: false, // 若是是https接口,須要配置這個參數
changeOrigin: true, // 若是接口跨域,須要進行這個參數配置
pathRewrite: {
'^/core': '/core'
}
}
},
複製代碼
'use strict'
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"http://localhost:8082/core/"',
}
複製代碼
export function login(username, password) {
return request({
url: '/auth/login',
method: 'post',
data: {
username,
password
}
})
}
複製代碼
以上差很少就是我在前端遇到的大問題,頗有不少小問題,就不一一貼了,設置了以上後,能夠先試一試能不能跑起來,若是不行,能夠對比我在GitHub的代碼。
當咱們再次登錄時,能夠看到咱們已是在數據庫中查詢用戶信息而且登錄了。
若是再以默認的admin登錄則顯示用戶名或密碼錯誤