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 相关文章推荐
csdn 博客中实现运行代码功能实现
Aug 29 Javascript
JQuery下的Live方法和$.browser方法使用代码
Jun 02 Javascript
jquery ui dialog ie8出现滚动条的解决方法
Dec 06 Javascript
很棒的学习jQuery的12个网站推荐
Apr 28 Javascript
jQuery load方法用法集锦
Dec 06 Javascript
cookie在javascript中的使用技巧以及隐私在服务器端的设置
Dec 03 Javascript
js switch case default 的用法示例介绍
Oct 23 Javascript
confirm的用法示例用于按钮操作时确定是否执行
Jun 19 Javascript
JavaScript的类型、值和变量小结
Jul 09 Javascript
webpack打包多页面的方法
Nov 30 Javascript
vue组件定义,全局、局部组件,配合模板及动态组件功能示例
Mar 19 Javascript
vue实现codemirror代码编辑器中的SQL代码格式化功能
Aug 27 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
超神学院:鹤熙已踏入神圣领域,实力不比凯莎弱
2020/03/02 国漫
php基础知识:类与对象(4) 范围解析操作符(::)
2006/12/13 PHP
从零开始学YII2框架(五)快速生成代码工具 Gii 的使用
2014/08/20 PHP
汇总PHPmailer群发Gmail的常见问题
2016/02/24 PHP
如何在PHP中生成随机数
2020/06/04 PHP
Javascript优化技巧(文件瘦身篇)
2008/01/28 Javascript
js的逻辑运算符 ||
2010/05/31 Javascript
Ext JS 4实现带week(星期)的日期选择控件(实战二)
2013/08/21 Javascript
javascript实现控制浏览器全屏
2015/03/30 Javascript
js代码验证手机号码和电话号码是否合法
2015/07/30 Javascript
js采用concat和sort将N个数组拼接起来的方法
2016/01/21 Javascript
JavaScript中this的用法实例分析
2016/12/19 Javascript
Angular.js中处理页面闪烁的方法详解
2017/03/09 Javascript
vue计算属性时v-for处理数组时遇到的一个bug问题
2018/01/21 Javascript
vue-cli常用设置总结
2018/02/24 Javascript
JavaScript使用math.js进行精确计算操作示例
2018/06/19 Javascript
[02:43]2014DOTA2国际邀请赛 官方Alliance战队纪录片
2014/07/14 DOTA
[14:24]Optic Gaming vs PSG LGD BO3
2018/06/07 DOTA
在Python中使用__slots__方法的详细教程
2015/04/28 Python
Python结巴中文分词工具使用过程中遇到的问题及解决方法
2017/04/15 Python
浅谈Python的list中的选取范围
2018/11/12 Python
一个可以套路别人的python小程序实例代码
2019/04/09 Python
用python的turtle模块实现给女票画个小心心
2019/11/23 Python
python sorted函数原理解析及练习
2020/02/10 Python
django inspectdb 操作已有数据库数据的使用步骤
2021/02/07 Python
CSS3 实现的缩略图悬停效果
2020/12/09 HTML / CSS
路德维希•贝克(LUDWIG BECK)中文官网:德国大型美妆百货
2020/09/19 全球购物
internal修饰符起什么作用
2013/12/16 面试题
软件专业毕业生个人自我鉴定
2014/04/17 职场文书
企业指导教师评语
2014/04/28 职场文书
工作失职自我检讨书
2015/05/05 职场文书
正规欠条模板
2015/07/03 职场文书
六五普法先进个人主要事迹材料
2015/11/03 职场文书
Redis keys命令的具体使用
2022/06/05 Redis
MySQL详解进行JDBC编程与增删改查方法
2022/06/16 MySQL
CSS link与@import的区别和用法解析
2023/05/07 HTML / CSS