项目地址: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,没有携带的不予通过,再加上网上很多都是通过这种模式来实现的,参考的资料也比较多。


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的拦截器链中去了,这就使他们俩有了合作的关系,接下来就是创建我们的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登陆则显示用户名或密码错误