本篇作为SpringBoot2.1版本的个人开发框架 子章节,请先阅读 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大神希望是一个集成方案,当我们需要什么再去拿什么。
这里我不对项目结构做介绍,在大神的系列文章中都有介绍,当我们把项目下载好以后,我们尝试的在本地跑起来,确定没有错误以后我们再进行下一步。
在下载好的项目中运行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(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的拦截器链中去了,这就使他们俩有了合作的关系,接下来就是创建我们的 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的代码。