Vue+Jwt+SpringBoot+Ldap完成登錄認(rèn)證的示例代碼
本人野生程序員一名,了解了一些微服務(wù)架構(gòu)、前后端分離、SPA的知識(shí)后就想試試做點(diǎn)什么東西。之前一直做后端,前端只是有基礎(chǔ)知識(shí)。之前學(xué)習(xí)過(guò)angularjs,但當(dāng)時(shí)就是一臉懵逼(完全看不懂是啥)就放棄了。最近又學(xué)了Vue,這次感覺(jué)總算明白了一些,但其中也跳過(guò)很多坑(應(yīng)該還會(huì)更多),在這里寫(xiě)下來(lái)記錄一下吧。
說(shuō)回主題,之前傳統(tǒng)登錄認(rèn)證的方法基本是由服務(wù)器端提供一個(gè)登錄頁(yè)面,頁(yè)面中的一個(gè)form輸入username和password后POST給服務(wù)器,服務(wù)器將這些信息與DB或Ldap中的用戶(hù)信息對(duì)比,成功則將這個(gè)用戶(hù)信息記錄到session中。
這里我就跳了第一個(gè)大坑。傳統(tǒng)方式前后端不分離,后端負(fù)責(zé)頁(yè)面渲染,但是在前后分離的情況下,后端只負(fù)責(zé)通過(guò)暴露的RestApi提供數(shù)據(jù),而頁(yè)面的渲染、路由都由前端完成。因?yàn)閞est是無(wú)狀態(tài)的,因此也就不會(huì)有session記錄到服務(wù)器端。
之前一直使用SpringSecurity+Cas+Ldap來(lái)做SSO,但是使用Vue做前端后我怎都想不出用之前的方法做SSO(這個(gè)坑真的爬了好久才爬出來(lái))。后來(lái)終于想明白了上面說(shuō)的session的問(wèn)題(我是這么認(rèn)為的也可能不對(duì),CAS也有RestApi,但是按官網(wǎng)配置沒(méi)成功,放棄了)。
第一個(gè)問(wèn)題,該如何解決SSO的問(wèn)題呢,要說(shuō)到JWT。JWT是個(gè)規(guī)范,各種語(yǔ)言有各種語(yǔ)言的實(shí)現(xiàn),可以去官網(wǎng)查到。我淺薄的理解是有一個(gè)認(rèn)證服務(wù)(你自己寫(xiě)的,Db、Ldap什么都可以)這個(gè)認(rèn)證服務(wù)會(huì)通過(guò)用戶(hù)的提交信息判斷認(rèn)證是否成功,如果成功則查詢(xún)出一些用戶(hù)的信息(用戶(hù)名、角色什么的),然后JWT把這些信息加密成為一個(gè)token,返回給客戶(hù)端瀏覽器,瀏覽器把這些信息存儲(chǔ)在localstorage中,以后每次訪問(wèn)資源都會(huì)在header中攜帶這個(gè)信息,服務(wù)器收到請(qǐng)求后使用和加密時(shí)相同的key解密密文,如果解密成功則視為用戶(hù)已經(jīng)認(rèn)證過(guò)(當(dāng)然你可以在加密時(shí)添加以一個(gè)過(guò)期時(shí)間)也就完成了SSO。使用解密出的角色信息你就可以判斷這個(gè)用戶(hù)是否有權(quán)限執(zhí)行一些業(yè)務(wù)。這樣做完后感覺(jué)好像SpringSecurity、Cas在SPA應(yīng)用中的SSO似乎沒(méi)什么作用了,目前我是這么認(rèn)為的(當(dāng)然可能不對(duì))
第一個(gè)問(wèn)題差不多解決了,來(lái)說(shuō)第二個(gè)問(wèn)題。之前因?yàn)橛衧ession的存在,在訪問(wèn)受保護(hù)的資源時(shí)如果服務(wù)器端沒(méi)有當(dāng)前用戶(hù)的session,則會(huì)強(qiáng)制跳轉(zhuǎn)到登錄頁(yè)。那在前后分離的情況下要如何實(shí)現(xiàn)這個(gè)需求。思路是這樣的:利用Vue-Router的全局路由鉤子,在訪問(wèn)任何頁(yè)面時(shí)先判斷l(xiāng)ocalStorage中是否存在JWT加密后的token并且token是否過(guò)期,如果存在且沒(méi)有過(guò)期則正常跳轉(zhuǎn)到請(qǐng)求的頁(yè)面,不存在或者過(guò)期則跳轉(zhuǎn)到登錄頁(yè)重新認(rèn)證。
思路說(shuō)完了,上代碼
1.首先你需要一個(gè)Ldap,我使用的是AD。這里我建立了一個(gè)叫minibox.com的域,并且添加了一個(gè)Employees的OU,其中有2個(gè)子OU,子OU中創(chuàng)建了2個(gè)用戶(hù)。

在Groups中新建一些組,把之前創(chuàng)建的用戶(hù)加入到組中,這樣用戶(hù)就擁有了角色。

2.搭建SpringBoot環(huán)境
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>
<!-- 熱啟動(dòng) -->
<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應(yīng)用配置文件
#Logging_config logging.level.root=INFO logging.level.org.springframework.web=WARN logging.file=minibox.log #server_config #使用了SSL,并且在ldap配置中使用了ldaps,這里同時(shí)也需要把AD的證書(shū)導(dǎo)入到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加解密時(shí)使用的key jwt.key=minibox #ldap_config #ldap配置信息,注意這里的userDn一定要寫(xiě)這種形式。referral設(shè)置為follow,說(shuō)不清用途,似乎只有連接AD時(shí)才需要配置 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主配置類(lèi)
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//相當(dāng)于@Configuration,@EnableAutoConfiguration,@ComponentScan
public class Application {
/*
* SpringLdap配置。通過(guò)@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提供認(rèn)證服務(wù)的類(lèi)
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;
/**
* 將域用戶(hù)屬性通過(guò)EmployeeAttributesMapper填充到Employee類(lèi)中,返回一個(gè)填充信息的Employee實(shí)例
*/
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 用戶(hù)提交的名稱(chēng)
* @param password 用戶(hù)提交的密碼
* @return 成功返回加密后的token信息,失敗返回錯(cuò)誤HTTP狀態(tài)碼
*/
@CrossOrigin//因?yàn)樾枰缬蛟L問(wèn),所以要加這個(gè)注解
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<String> authByAd(
@RequestParam(value = "username") String username,
@RequestParam(value = "password") String password) {
//這里注意用戶(hù)名加域名后綴 userDn格式:anwx@minibox.com
String userDn = username + ldapDomainName;
//token過(guò)期時(shí)間 4小時(shí)
Date tokenExpired = new Date(new Date().getTime() + 60*60*4*1000);
DirContext ctx = null;
try {
//使用用戶(hù)名、密碼驗(yàn)證域用戶(hù)
ctx = ldapTemplate.getContextSource().getContext(userDn, password);
//如果驗(yàn)證成功根據(jù)sAMAccountName屬性查詢(xún)用戶(hù)名和用戶(hù)所屬的組
Employee employee = ldapTemplate .search(query().where("objectclass").is("person").and("sAMAccountName").is(username),
new EmployeeAttributesMapper())
.get(0);
//使用Jwt加密用戶(hù)名和用戶(hù)所屬組信息
String compactJws = Jwts.builder()
.setSubject(employee.getName())
.setAudience(employee.getRole())
.setExpiration(tokenExpired)
.signWith(SignatureAlgorithm.HS512, jwtKey).compact();
//登錄成功,返回客戶(hù)端token信息。這里只加密了用戶(hù)名和用戶(hù)角色,而displayName和tokenExpired沒(méi)有加密
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狀態(tài)碼
return new ResponseEntity<String>(HttpStatus.UNAUTHORIZED);
} finally {
//關(guān)閉ldap連接
LdapUtils.closeContext(ctx);
}
}
}
4.前端Vue
4.1使用Vue-cli搭建項(xiàng)目,并使用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默認(rèn)以payload方式提交數(shù)據(jù),這樣設(shè)置之后以formData方式提交
Vue.http.options.emulateJSON = true;
const routes = [
{
path: '/login',
component : Login
},{
path: '/hello',
component: Hello
}
]
const router = new VueRouter({
routes
})
//默認(rèn)導(dǎo)航到登錄頁(yè)
router.push('/login')
/*
全局路由鉤子
訪問(wèn)資源時(shí)需要驗(yàn)證localStorage中是否存在token
以及token是否過(guò)期
驗(yàn)證成功可以繼續(xù)跳轉(zhuǎn)
失敗返回登錄頁(yè)重新登錄
*/
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">用戶(hù)名或密碼錯(cuò)誤</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>
//提供認(rèn)證服務(wù)的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
認(rèn)證成功將返回的用戶(hù)信息寫(xiě)入到localStorage,并跳轉(zhuǎn)到下一頁(yè)面
失敗提示認(rèn)證錯(cuò)誤
*/
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訪問(wèn)http://localhost:8000時(shí)被導(dǎo)航到登錄頁(yè)

5.2提交登錄信息并取得token,跳轉(zhuǎn)下一頁(yè)

到這里整個(gè)功能就完成了。本人也是菜鳥(niǎo)一枚,理解有錯(cuò)誤的地方還請(qǐng)各位老師指正。打算把整個(gè)分布式系統(tǒng)的開(kāi)發(fā)過(guò)程記錄下來(lái)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- SpringBoot整合Shiro實(shí)現(xiàn)登錄認(rèn)證的方法
- springboot前后端分離集成CAS單點(diǎn)登錄(統(tǒng)一認(rèn)證)
- SpringBoot+Vue+JWT的前后端分離登錄認(rèn)證詳細(xì)步驟
- SpringBoot?實(shí)現(xiàn)CAS?Server統(tǒng)一登錄認(rèn)證的詳細(xì)步驟
- 基于springboot實(shí)現(xiàn)整合shiro實(shí)現(xiàn)登錄認(rèn)證以及授權(quán)過(guò)程解析
- Springboot+Spring Security實(shí)現(xiàn)前后端分離登錄認(rèn)證及權(quán)限控制的示例代碼
- SpringBoot整合Sa-Token實(shí)現(xiàn)登錄認(rèn)證的示例代碼
- Springboot整合SpringSecurity實(shí)現(xiàn)登錄認(rèn)證和鑒權(quán)全過(guò)程
- SpringBoot+MyBatis Plus實(shí)現(xiàn)用戶(hù)登錄認(rèn)證
相關(guān)文章
vue 解決在微信內(nèi)置瀏覽器中調(diào)用支付寶支付的情況
這篇文章主要介紹了vue 解決在微信內(nèi)置瀏覽器中調(diào)用支付寶支付的情況,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11
Vue3?實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)功能
這篇文章主要介紹了Vue3?實(shí)現(xiàn)驗(yàn)證碼倒計(jì)時(shí)功能,倒計(jì)時(shí)的運(yùn)用場(chǎng)景是獲取手機(jī)驗(yàn)證碼倒計(jì)時(shí)、獲取郵箱驗(yàn)證碼倒計(jì)時(shí)等場(chǎng)景,本文結(jié)合示例代碼給大家詳細(xì)講解,需要的朋友可以參考下2023-01-01
Vue+elementui 實(shí)現(xiàn)復(fù)雜表頭和動(dòng)態(tài)增加列的二維表格功能
這篇文章主要介紹了Vue+elementui 實(shí)現(xiàn)復(fù)雜表頭和動(dòng)態(tài)增加列的二維表格功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09
vue-cli3.0.4中webpack的dist路徑如何修改
這篇文章主要介紹了vue-cli3.0.4中webpack的dist路徑如何修改,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
如何在vue3中同時(shí)使用tsx與setup語(yǔ)法糖
這篇文章主要介紹了如何在vue3中同時(shí)使用tsx與setup語(yǔ)法糖,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-09-09
VUE 項(xiàng)目在IE11白屏報(bào)錯(cuò) SCRIPT1002: 語(yǔ)法錯(cuò)誤的解決
這篇文章主要介紹了VUE 項(xiàng)目在IE11白屏報(bào)錯(cuò) SCRIPT1002: 語(yǔ)法錯(cuò)誤的解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
解決在Vue中使用axios POST請(qǐng)求變成OPTIONS的問(wèn)題
這篇文章主要介紹了解決在Vue中使用axios POST請(qǐng)求變成OPTIONS的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
element-ui中this.$confirm提示文字中部分有顏色自定義方法
this.$confirm是一個(gè)Vue.js中的彈窗組件,其樣式可以通過(guò)CSS進(jìn)行自定義,下面這篇文章主要給大家介紹了關(guān)于element-ui中this.$confirm提示文字中部分有顏色的自定義方法,需要的朋友可以參考下2024-02-02
Element中的Cascader(級(jí)聯(lián)列表)動(dòng)態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法
這篇文章主要介紹了Element中的Cascader(級(jí)聯(lián)列表)動(dòng)態(tài)加載省\市\(zhòng)區(qū)數(shù)據(jù)的方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03

