Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码


Posted in Javascript onMay 21, 2018

本人野生程序员一名,了解了一些微服务架构、前后端分离、SPA的知识后就想试试做点什么东西。之前一直做后端,前端只是有基础知识。之前学习过angularjs,但当时就是一脸懵逼(完全看不懂是啥)就放弃了。最近又学了Vue,这次感觉总算明白了一些,但其中也跳过很多坑(应该还会更多),在这里写下来记录一下吧。

说回主题,之前传统登录认证的方法基本是由服务器端提供一个登录页面,页面中的一个form输入username和password后POST给服务器,服务器将这些信息与DB或Ldap中的用户信息对比,成功则将这个用户信息记录到session中。

这里我就跳了第一个大坑。传统方式前后端不分离,后端负责页面渲染,但是在前后分离的情况下,后端只负责通过暴露的RestApi提供数据,而页面的渲染、路由都由前端完成。因为rest是无状态的,因此也就不会有session记录到服务器端。

之前一直使用SpringSecurity+Cas+Ldap来做SSO,但是使用Vue做前端后我怎都想不出用之前的方法做SSO(这个坑真的爬了好久才爬出来)。后来终于想明白了上面说的session的问题(我是这么认为的也可能不对,CAS也有RestApi,但是按官网配置没成功,放弃了)。

第一个问题,该如何解决SSO的问题呢,要说到JWT。JWT是个规范,各种语言有各种语言的实现,可以去官网查到。我浅薄的理解是有一个认证服务(你自己写的,Db、Ldap什么都可以)这个认证服务会通过用户的提交信息判断认证是否成功,如果成功则查询出一些用户的信息(用户名、角色什么的),然后JWT把这些信息加密成为一个token,返回给客户端浏览器,浏览器把这些信息存储在localstorage中,以后每次访问资源都会在header中携带这个信息,服务器收到请求后使用和加密时相同的key解密密文,如果解密成功则视为用户已经认证过(当然你可以在加密时添加以一个过期时间)也就完成了SSO。使用解密出的角色信息你就可以判断这个用户是否有权限执行一些业务。这样做完后感觉好像SpringSecurity、Cas在SPA应用中的SSO似乎没什么作用了,目前我是这么认为的(当然可能不对)

第一个问题差不多解决了,来说第二个问题。之前因为有session的存在,在访问受保护的资源时如果服务器端没有当前用户的session,则会强制跳转到登录页。那在前后分离的情况下要如何实现这个需求。思路是这样的:利用Vue-Router的全局路由钩子,在访问任何页面时先判断localStorage中是否存在JWT加密后的token并且token是否过期,如果存在且没有过期则正常跳转到请求的页面,不存在或者过期则跳转到登录页重新认证。

思路说完了,上代码

1.首先你需要一个Ldap,我使用的是AD。这里我建立了一个叫minibox.com的域,并且添加了一个Employees的OU,其中有2个子OU,子OU中创建了2个用户。

Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码

在Groups中新建一些组,把之前创建的用户加入到组中,这样用户就拥有了角色。

Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码

2.搭建SpringBoot环境

2.1pom文件

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>minibox</groupId>
 <artifactId>an</artifactId>
 <version>0.0.1-SNAPSHOT</version>
  <!-- Inherit defaults from Spring Boot -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.1.RELEASE</version>
  </parent>
  <!-- Add typical dependencies for a web application -->
  <dependencies>
    <!-- MVC -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring boot test -->
    <dependency> 
      <groupId>org.springframework.boot</groupId> 
      <artifactId>spring-boot-starter-test</artifactId> 
      <scope>test</scope> 
    </dependency> 
    <!-- spring-boot-starter-hateoas -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-hateoas</artifactId>
    </dependency>
    <!-- 热启动 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <optional>true</optional>
    </dependency>
    <!-- JWT -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.7.0</version>
    </dependency>
    <!-- Spring Ldap -->
    <dependency>
      <groupId>org.springframework.ldap</groupId>
      <artifactId>spring-ldap-core</artifactId>
      <version>2.3.1.RELEASE</version>
    </dependency>
    <!-- fastjson -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.24</version>
    </dependency>
  </dependencies>
  <!-- Package as an executable jar -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- Hot swapping -->
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <dependencies>
          <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>springloaded</artifactId>
            <version>1.2.0.RELEASE</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>
</project>

2.2应用配置文件

#Logging_config
logging.level.root=INFO
logging.level.org.springframework.web=WARN
logging.file=minibox.log

#server_config
#使用了SSL,并且在ldap配置中使用了ldaps,这里同时也需要把AD的证书导入到server.keystore中。具体的可以查看java的keytool工具
server.port=8443
server.ssl.key-store=classpath:server.keystore
server.ssl.key-store-password=minibox
server.ssl.key-password=minibox

#jwt
#jwt加解密时使用的key
jwt.key=minibox

#ldap_config
#ldap配置信息,注意这里的userDn一定要写这种形式。referral设置为follow,说不清用途,似乎只有连接AD时才需要配置
ldap.url=ldaps://192.168.227.128:636
ldap.base=ou=Employees,dc=minibox,dc=com
ldap.userDn=cn=Administrator,cn=Users,dc=minibox,dc=com
ldap.userPwd=qqq111!!!!
ldap.referral=follow
ldap.domainName=@minibox.com

3.Spring主配置类

package an;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
@SpringBootApplication//相当于@Configuration,@EnableAutoConfiguration,@ComponentScan
public class Application {

  /*
  * SpringLdap配置。通过@Value注解读取之前配置文件中的值
  */
  @Value("${ldap.url}")
  private String ldapUrl;

  @Value("${ldap.base}")
  private String ldapBase;

  @Value("${ldap.userDn}")
  private String ldapUserDn;

  @Value("${ldap.userPwd}")
  private String ldapUserPwd;

  @Value("${ldap.referral}")
  private String ldapReferral;

  /*
  *SpringLdap的javaConfig注入方式
  */
  @Bean
  public LdapTemplate ldapTemplate() {
    return new LdapTemplate(contextSourceTarget());
  }

  @Bean
  public LdapContextSource contextSourceTarget() {
    LdapContextSource ldapContextSource = new LdapContextSource();
    ldapContextSource.setUrl(ldapUrl);
    ldapContextSource.setBase(ldapBase);
    ldapContextSource.setUserDn(ldapUserDn);
    ldapContextSource.setPassword(ldapUserPwd);
    ldapContextSource.setReferral(ldapReferral);
    return ldapContextSource;
  }

  public static void main(String[] args) throws Exception {
    SpringApplication.run(Application.class, args);
  }
}

3.1提供认证服务的类

package an.auth;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ldap.NamingException;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import an.entity.Employee;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@RestController
@RequestMapping("/auth")
public class JwtAuth {

  //jwt加密密匙
  @Value("${jwt.key}")
  private String jwtKey;

  //域名后缀
  @Value("${ldap.domainName}")
  private String ldapDomainName;

  //ldap模板
  @Autowired
  private LdapTemplate ldapTemplate;

  /**
   * 将域用户属性通过EmployeeAttributesMapper填充到Employee类中,返回一个填充信息的Employee实例
   */
  private class EmployeeAttributesMapper implements AttributesMapper<Employee> {
    public Employee mapFromAttributes(Attributes attrs) throws NamingException, javax.naming.NamingException {
      Employee employee = new Employee();
      employee.setName((String) attrs.get("sAMAccountName").get());
      employee.setDisplayName((String) attrs.get("displayName").get());
      employee.setRole((String) attrs.get("memberOf").toString());
      return employee;
    }
  }

  /**
   * @param username 用户提交的名称
   * @param password 用户提交的密码
   * @return 成功返回加密后的token信息,失败返回错误HTTP状态码
   */
  @CrossOrigin//因为需要跨域访问,所以要加这个注解
  @RequestMapping(method = RequestMethod.POST)
  public ResponseEntity<String> authByAd(
      @RequestParam(value = "username") String username,
      @RequestParam(value = "password") String password) {
    //这里注意用户名加域名后缀 userDn格式:anwx@minibox.com
    String userDn = username + ldapDomainName;
    //token过期时间 4小时
    Date tokenExpired = new Date(new Date().getTime() + 60*60*4*1000);
    DirContext ctx = null;
    try {
      //使用用户名、密码验证域用户
      ctx = ldapTemplate.getContextSource().getContext(userDn, password);
      //如果验证成功根据sAMAccountName属性查询用户名和用户所属的组
      Employee employee = ldapTemplate                            .search(query().where("objectclass").is("person").and("sAMAccountName").is(username),
              new EmployeeAttributesMapper())
          .get(0);
      //使用Jwt加密用户名和用户所属组信息
      String compactJws = Jwts.builder()
          .setSubject(employee.getName())
          .setAudience(employee.getRole())
          .setExpiration(tokenExpired)
          .signWith(SignatureAlgorithm.HS512, jwtKey).compact();
      //登录成功,返回客户端token信息。这里只加密了用户名和用户角色,而displayName和tokenExpired没有加密
      Map<String, Object> userInfo = new HashMap<String, Object>();
      userInfo.put("token", compactJws);
      userInfo.put("displayName", employee.getDisplayName());
      userInfo.put("tokenExpired", tokenExpired.getTime());
      return new ResponseEntity<String>(JSON.toJSONString(userInfo , SerializerFeature.DisableCircularReferenceDetect) , HttpStatus.OK);
    } catch (Exception e) {
      //登录失败,返回失败HTTP状态码
      return new ResponseEntity<String>(HttpStatus.UNAUTHORIZED);
    } finally {
      //关闭ldap连接
      LdapUtils.closeContext(ctx);
    }
  }

}

4.前端Vue

4.1使用Vue-cli搭建项目,并使用vue-router和vue-resource,不了解的可以搜索下

4.2 main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import VueRouter from 'vue-router'
import VueResource from 'vue-resource'
import store from './store/store'
import 'bootstrap/dist/css/bootstrap.css'
import App from './App'
import Login from './components/login'
import Hello from './components/hello'

Vue.use(VueRouter)
Vue.use(VueResource)
//Vue-resource默认以payload方式提交数据,这样设置之后以formData方式提交
Vue.http.options.emulateJSON = true;

const routes = [
 {
  path: '/login',
  component : Login
 },{
  path: '/hello',
  component: Hello
 }
]

const router = new VueRouter({
 routes
})

//默认导航到登录页
router.push('/login')

/*
全局路由钩子
访问资源时需要验证localStorage中是否存在token
以及token是否过期
验证成功可以继续跳转
失败返回登录页重新登录
 */
router.beforeEach((to, from, next) => {
 if(localStorage.token && new Date().getTime() < localStorage.tokenExpired){
  next()
 }
 else{
  next('/login')
 }
})

new Vue({
 el: '#app',
 template: '<App/>',
 components: { App },
 router,
 store
})

4.3 App.vue

<template>
 <div id="app">
  <router-view></router-view>
 </div>
</template>

<script>
 export default {
  name: 'app',
 }
</script>

<style scoped>
</style>

4.4 login.vue

<template>
  <div class="login-box">
    <div class="login-logo">
      <b>Admin</b>LTE
    </div>
    <div class="login-box-body">
      <div class="input-group form-group has-feedback">
        <span class="input-group-addon"><span class="glyphicon glyphicon-user"></span></span>
        <input v-model="username" type="text" class="form-control" placeholder="username">
        <span class="input-group-addon">@minibox.com</span>
      </div>
      <div class="input-group form-group has-feedback">
        <span class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></span>
        <input v-model="password" type="password" class="form-control" placeholder="Password">
      </div>
      <div class="row">
        <div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3">
          <transition name="slide-fade">
            <p v-if="show">用户名或密码错误</p>
          </transition>
        </div>
      </div>
      <div class="row">
        <div class="col-sm-6 col-sm-offset-3 col-md-6 col-md-offset-3">
          <button v-on:click="auth" class="btn btn-primary btn-block btn-flat">Sign In</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  //提供认证服务的restApi
  var authUrl = 'https://192.168.227.1:8443/auth'
  export default {
    name: 'app',
    data() {
      return {
        username: '',
        password: '',
        show: false
      }
    },
    methods: {
      auth: function(){
        var credentials = {
          username:this.username,
          password:this.password
        }
        /*
        post方法提交username和password
        认证成功将返回的用户信息写入到localStorage,并跳转到下一页面
        失败提示认证错误
        */
        this.$http.post(authUrl, credentials).then(response => {
          localStorage.token = response.data.token
          localStorage.tokenExpired = response.data.tokenExpired
          localStorage.userDisplayName = response.data.displayName
          this.$router.push('hello')
        }, response => {
          this.show = true
        })
      }
    }
  }
</script>

<style scoped>
  p{
    text-align: center
  }
  .slide-fade-enter-active {
    transition: all .8s ease;
  }
  .slide-fade-leave-active {
    transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
  }
  .slide-fade-enter, .slide-fade-leave-to
  /* .slide-fade-leave-active for <2.1.8 */ {
    transform: translateX(10px);
    opacity: 0;
  }
  @import '../assets/css/AdminLTE.min.css'
</style>

5效果

5.1访问http://localhost:8000时被导航到登录页

Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码

5.2提交登录信息并取得token,跳转下一页

Vue+Jwt+SpringBoot+Ldap完成登录认证的示例代码

到这里整个功能就完成了。本人也是菜鸟一枚,理解有错误的地方还请各位老师指正。打算把整个分布式系统的开发过程记录下来。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jBox 2.3基于jquery的最新多功能对话框插件 常见使用问题解答
Nov 10 Javascript
浏览器解析js生成的html出现样式问题的解决方法
Apr 16 Javascript
jQuery实现获取table表格第一列值的方法
Mar 01 Javascript
深入理解JavaScript中的对象复制(Object Clone)
May 18 Javascript
Node.js+Express+MySql实现用户登录注册功能
Jul 10 Javascript
20170918 前端开发周报之JS前端开发必看
Sep 18 Javascript
Angular开发实践之服务端渲染
Mar 29 Javascript
利用Vue构造器创建Form组件的通用解决方法
Dec 03 Javascript
全面分析JavaScript 继承
May 30 Javascript
基于VUE实现简单的学生信息管理系统
Jan 13 Vue.js
Java 生成随机字符的示例代码
Jan 13 Javascript
quickjs 封装 JavaScript 沙箱详情
Nov 02 Javascript
纯JS实现可用于页码更换的飞页特效示例
May 21 #Javascript
JS实现的文件拖拽上传功能示例
May 21 #Javascript
以v-model与promise两种方式实现vue弹窗组件
May 21 #Javascript
Vue二次封装axios为插件使用详解
May 21 #Javascript
详解vue的diff算法原理
May 20 #Javascript
详解使用vue-admin-template的优化历程
May 20 #Javascript
vuex进阶知识点巩固
May 20 #Javascript
You might like
如何选购合适的收音机
2021/03/01 无线电
我的论坛源代码(三)
2006/10/09 PHP
PHP Stream_*系列函数
2010/08/01 PHP
php对象在内存中的存在形式分析
2015/02/03 PHP
php简单smarty入门程序实例
2015/06/11 PHP
php实现复制移动文件的方法
2015/07/29 PHP
Thinkphp 3.2框架使用Redis的方法详解
2019/10/24 PHP
PHP生成图表pChart的示例解析
2020/07/31 PHP
jQuery当鼠标悬停时放大图片的效果实例
2013/07/03 Javascript
node.js中的dns.getServers方法使用说明
2014/12/08 Javascript
JS制作简单的三级联动
2015/03/18 Javascript
JavaScript实现上下浮动的窗口效果代码
2015/10/12 Javascript
jQuery on()方法绑定动态元素的点击事件实例代码浅析
2016/06/16 Javascript
js HTML5多图片上传及预览实例解析(不含前端的文件分割)
2016/08/26 Javascript
详解JavaScript按概率随机生成事件
2017/08/02 Javascript
用node-webkit把web应用打包成桌面应用(windows环境)
2018/02/01 Javascript
浅谈使用mpvue开发小程序需要注意和了解的知识点
2018/05/23 Javascript
实例分析编写vue组件方法
2019/02/12 Javascript
JavaScript中的this/call/apply/bind的使用及区别
2020/03/06 Javascript
taro 实现购物车逻辑的实例代码
2020/06/05 Javascript
python显示生日是星期几的方法
2015/05/27 Python
python实现壁纸批量下载代码实例
2018/01/25 Python
python re模块匹配贪婪和非贪婪模式详解
2020/02/11 Python
使用python执行shell脚本 并动态传参 及subprocess的使用详解
2020/03/06 Python
Django 拼接两个queryset 或是两个不可以相加的对象实例
2020/03/28 Python
打印tensorflow恢复模型中所有变量与操作节点方式
2020/05/26 Python
HTML5 Canvas 破碎重组的视频特效的示例代码
2019/09/24 HTML / CSS
瑞典时尚耳机品牌:Urbanears
2017/07/26 全球购物
利物浦足球俱乐部官方商店(美国):Liverpool FC US
2019/10/09 全球购物
安全责任协议书
2014/04/21 职场文书
实习感想范文
2015/08/10 职场文书
团队执行力培训心得体会
2015/08/15 职场文书
CSS中Single Div 绘图技巧的实现
2021/06/18 HTML / CSS
Java 在线考试云平台的实现
2021/11/23 Java/Android
Golang 对es的操作实例
2022/04/20 Golang
Python测试框架pytest高阶用法全面详解
2022/06/01 Python