ホームページ  >  記事  >  ウェブフロントエンド  >  Vue+Jwt+SpringBoot+Ldapを使ってログイン認証を完了する方法

Vue+Jwt+SpringBoot+Ldapを使ってログイン認証を完了する方法

php中世界最好的语言
php中世界最好的语言オリジナル
2018-05-28 14:56:263951ブラウズ

今回は、Vue+Jwt+SpringBoot+Ldapを使用してログイン認証を完了する方法と、Vue+Jwt+SpringBoot+Ldapを使用してログイン認証を完了する際の注意点を説明します。 以下は実際のケースです。見てみましょう。

以前の従来のログイン認証方法は、基本的にサーバー側でログイン ページを提供し、ページ上のフォームにユーザー名とパスワードを入力し、サーバーがこの情報を DB または LDAP のユーザー情報と比較しました。 、このユーザー情報はセッションに記録されます。

ここで最初の大きな穴に飛び込みました。従来の方法では、フロント エンドとバック エンドは分離されておらず、バック エンドがページのレンダリングを担当します。ただし、フロント エンドとバック エンドが分離されている場合、バック エンドは公開された RestApi を介してデータを提供することのみを担当します。ページのレンダリングとルーティングはフロントエンドによって完了します。 REST はステートレスであるため、サーバーにセッションは記録されません。

以前は SpringSecurity+Cas+Ldap を使用して SSO を実行していましたが、フロントエンドとして Vue を使用してからは、以前の方法を使用して SSO を実行することは考えられませんでした (状態から抜け出すのに長い時間がかかりました)この穴)。その後、上記のセッションの問題がようやくわかりました(私はそう思っていましたが、間違っている可能性があります。CASにはRestApiもありますが、公式Webサイトに従って設定できなかったので、あきらめました)。

最初の質問は、SSO の問題を解決する方法です。JWT について話しましょう。 JWTは仕様であり、さまざまな言語での実装があり、公式Webサイトで確認できます。私の表面的な理解は、認証サービス(自分で作成した、Db、LDAP、何でも)があり、この認証サービスは、ユーザーが送信した情報に基づいて認証が成功したかどうかを判断し、成功した場合は、いくつかのユーザー情報(ユーザー)を照会します。名前、ロールなど)、JWT はこの情報をトークンに暗号化し、クライアント ブラウザーにこの情報を格納し、今後リソースにアクセスするたびにこの情報をヘッダーに含めます。サーバーはリクエストを受信した後、暗号文を暗号化時と同じキーで復号化します。復号化に成功すると、ユーザーは認証されたとみなされます (もちろん、暗号化中に有効期限を追加することもできます)。完成されました。復号化されたロール情報を使用して、このユーザーが一部のサービスを実行する権限を持っているかどうかを判断できます。これを実行すると、SPA アプリケーションの SSO には SpringSecurity と Cas が役割を持たないように感じます (もちろん間違っている可能性もあります)

最初の問題はほぼ解決したので、2 番目の問題について話しましょう。 。以前は、セッションの存在により、保護されたリソースにアクセスするときにサーバーに現在のユーザーのセッションが存在しない場合、強制的にログイン ページにジャンプしていました。フロントとリアが分離されている場合、この要件をどのように達成するか。考え方は次のとおりです。Vue-Router のグローバル ルーティング フックを使用して、まず localStorage に JWT 暗号化トークンがあるかどうか、またトークンが存在し期限切れでない場合はそのトークンの有効期限が切れているかどうかを判断します。存在しない場合、または有効期限が切れている場合は、再認証のためにログイン ページにジャンプします。

アイデアはこれくらいにして、コードに進みましょう

1. まず、LDAP が必要です。私は AD を使用します。ここでは、minibox.com というドメインを作成し、2 つのサブ OU を持つ Employees OU を追加し、サブ OU に 2 人のユーザーを作成しました。

グループでいくつかの新しいグループを作成し、以前に作成したユーザーをグループに追加して、ユーザーに役割を与えます。

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(&#39;/login&#39;)
 }
})
new Vue({
 el: &#39;#app&#39;,
 template: &#39;<App/>',
 components: { App },
 router,
 store
})

4.3 App.vue

<template>
 <p id="app">
  <router-view></router-view>
 </p>
</template>
<script>
 export default {
  name: 'app',
 }
</script>
<style scoped>
</style>

4.4 login.vue

<template>
  <p class="login-box">
    <p class="login-logo">
      <b>Admin</b>LTE
    </p>
    <p class="login-box-body">
      <p 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>
      </p>
      <p 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">
      </p>
      <p class="row">
        <p 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>
        </p>
      </p>
      <p class="row">
        <p 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>
        </p>
      </p>
    </p>
  </p>
</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にアクセスすると、ログインページに移動します

5.2 ログイン情報を送信してトークンを取得し、次のページにジャンプします

この記事の事例を読んだ後、あなたはその方法をマスターしたと思います。さらにエキサイティングな内容については、php 中国語 Web サイトの他の関連記事にご注意ください。

推奨読書:

Vueを使用してAdminLTEテンプレートを統合する方法

JSで三目並べゲームを作成する方法

以上がVue+Jwt+SpringBoot+Ldapを使ってログイン認証を完了する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。