本篇作为SpringBoot2.1版本的个人开发框架 子章节,请先阅读SpringBoot2.1版本的个人开发框架再次阅读本篇文章
感谢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登陆则显示用户名或密码错误