阅读 1296

超详细的Spring Security OAuth2 JWT (SSO)整合项目

Spring Security OAuth2 JWT SSO 整合项目

前言

本项目为Spring Security OAuth2 JWT SSO整合项目。适合对Spring Security、OAuth2和JWT SSO有一定认识并且想要进行有机整合的各位,项目本着上手最简单,基础功能最完善的原则编写。 基于数据库的认证和鉴权。采用OAuth2认证,根据认证服务器端查询的用户信息,进行认证处理,根据权限在资源服务器进行鉴权。此处采用权限鉴权模式,根据用户的权限,开闸可以访问的资源服务器范围。【另外还有根据角色鉴权,只是换汤不换药】

for (int i = 0; i <= 2; i++) {

System.out.println("直接下载源码食用更佳!源码在文章最下方给出!");

}

源码已经进行详细的注解,可以结合文章一起理解。

举个栗子稍微理解一下Oauth2认证,网站使用微信认证的过程:(从上到下按按照箭头方向进行) 在这里插入图片描述

一、使用步骤

1.工程结构

在这里插入图片描述

2.添加依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xxxx</groupId>
    <artifactId>springsecurityoauth2-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springsecurityoauth2-demo</name>
    <description>Demo project for Spring Boot</description>
    <!--设置spring cloud以及jdk版本变量-->
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <!--oauth2和security使用spring cloud里面的组件-->
        <!--只有引入了spring cloud依赖以及版本后才会生效-->
        <dependency>
            <!--oauth2依赖-->
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <!--security依赖-->
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <!--web依赖-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <!--test依赖-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
            <!--jwt 依赖-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok
            </groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <!--    MyBatis-Spring-Boot-Starter类似一个中间件,链接Spring Boot和MyBatis,构建基于Spring Boot的MyBatis应用程序-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--    mysql-connector-java 是MySQL提供的JDBC驱动包,用JDBC连接MySQL数据库时必须使用该jar包-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <!--dependencyManagement利用版本变量引入spring cloud依赖。-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <!--          mybatis-generator是mybatis自动生成实体代码的插件-->
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <configuration>
                    <verbose>true</verbose>
                    <overwrite>true</overwrite>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>
复制代码

3.数据库建表语句

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for um_t_role
-- ----------------------------
DROP TABLE IF EXISTS `um_t_role`;
CREATE TABLE `um_t_role`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `created_time` bigint(0) NOT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of um_t_role
-- ----------------------------
INSERT INTO `um_t_role` VALUES (1, '管理员拥有所有接口操作权限', 1627199362, '管理员', 'ADMIN');
INSERT INTO `um_t_role` VALUES (2, '普通拥有查看用户列表与修改密码权限,不具备对用户增删改权限', 1627199362, '普通用户', 'USER');

-- ----------------------------
-- Table structure for um_t_role_user
-- ----------------------------
DROP TABLE IF EXISTS `um_t_role_user`;
CREATE TABLE `um_t_role_user`  (
  `role_id` int(0) NULL DEFAULT NULL,
  `user_id` int(0) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of um_t_role_user
-- ----------------------------
INSERT INTO `um_t_role_user` VALUES (1, 1);

-- ----------------------------
-- Table structure for um_t_user
-- ----------------------------
DROP TABLE IF EXISTS `um_t_user`;
CREATE TABLE `um_t_user`  (
  `id` int(0) NOT NULL AUTO_INCREMENT,
  `account` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of um_t_user
-- ----------------------------
INSERT INTO `um_t_user` VALUES (1, 'admin', '系统默认管理员', '$2a$10$N97RyMYeQ7aVTxLvdxq5NeBivdbj/u2GQtHERISUt8qhKBfnjSC1q', 'admin');
INSERT INTO `um_t_user` VALUES (2, 'user', '普通用户', '$2a$10$N97RyMYeQ7aVTxLvdxq5NeBivdbj/u2GQtHERISUt8qhKBfnjSC1q', 'user');
INSERT INTO `um_t_user` VALUES (3, 'user', 'test user', '$2a$10$N97RyMYeQ7aVTxLvdxq5NeBivdbj/u2GQtHERISUt8qhKBfnjSC1q', 'Jacks');

SET FOREIGN_KEY_CHECKS = 1;
复制代码

==所有用户密码都是123456==

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

4.编写相关配置文件

(1)generatorConfig.xml 是mybatis-generator的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
  <!-- generatorConfig.xml配置文件,放在resource目录下即可 -->
  <!--数据库驱动个人配置-->
  <classPathEntry
    location="D:\maven-repo\mysql\mysql-connector-java\8.0.18\mysql-connector-java-8.0.18.jar"/>
  <context id="MysqlTables" targetRuntime="MyBatis3">
    <property name="autoDelimitKeywords" value="true"/>
    <!--可以使用``包括字段名,避免字段名与sql保留字冲突报错-->
    <property name="beginningDelimiter" value="`"/>
    <property name="endingDelimiter" value="`"/>
    <!-- optional,旨在创建class时,对注释进行控制 -->
    <commentGenerator>
      <property name="suppressDate" value="true"/>
      <property name="suppressAllComments" value="true"/>
    </commentGenerator>
    <!--数据库链接地址账号密码-->
    <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
      connectionURL="jdbc:mysql://127.0.0.1:3306/auth_test?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=Asia/Shanghai"
      userId="root"
      password="123456">
      <property name="nullCatalogMeansCurrent" value="true"/>
    </jdbcConnection>
    <!-- 非必需,类型处理器,在数据库类型和java类型之间的转换控制-->
    <javaTypeResolver>
      <property name="forceBigDecimals" value="false"/>
    </javaTypeResolver>
    <!--生成Model类存放位置-->
    <javaModelGenerator targetPackage="com.xxxx.springsecurityoauth2demo.model.pojo"
      targetProject="src/main/java">
      <!-- 是否允许子包,即targetPackage.schemaName.tableName -->
      <property name="enableSubPackages" value="true"/>
      <!-- 是否对类CHAR类型的列的数据进行trim操作 -->
      <property name="trimStrings" value="true"/>
      <!-- 建立的Model对象是否 不可改变  即生成的Model对象不会有 setter方法,只有构造方法 -->
      <property name="immutable" value="false"/>
    </javaModelGenerator>
    <!--生成mapper映射文件存放位置-->
    <sqlMapGenerator targetPackage="mappers" targetProject="src/main/resources">
      <property name="enableSubPackages" value="true"/>
    </sqlMapGenerator>
    <!--生成Dao类存放位置-->
    <javaClientGenerator type="XMLMAPPER" targetPackage="com.xxxx.springsecurityoauth2demo.model.dao"
      targetProject="src/main/java">
      <property name="enableSubPackages" value="true"/>
    </javaClientGenerator>
    <!--生成对应表及类名 schema 指数据库的用户名-->
    <table schema="root" tableName="um_t_role" domainObjectName="Role"
      enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>
    <table schema="root" tableName="um_t_user" domainObjectName="User" enableCountByExample="false"
      enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false"
      selectByExampleQueryId="false">
    </table>

  </context>
</generatorConfiguration>
复制代码

(2)application.properties

server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/auth_test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456

mybatis.mapper-locations=classpath:mappers/*.xml
复制代码

5.利用mybatis-generator生成实体类、Mapper、XML文档。

在这里插入图片描述

6.Security、OAuth2、JWT、SSO配置类

(1)授权服务器

用来进行授权配置。需要继承AuthorizationServerConfigurerAdapter类,重写configure()方法.

AuthorizationServerConfig.java

package com.xxxx.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

/**
 * 描述:授权服务器 @EnableAuthorizationServer,extends AuthorizationServerConfigurerAdapter
 * 为了模拟,授权服务器和资源服务器放在了一起,正常情况是解耦的。
 */
@Configuration
@EnableAuthorizationServer //开启授权服务器
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private UserDetailsService userDetailsService;
    @Resource(name = "jwtTokenStore")
    private TokenStore tokenStore;
    @Resource(name = "jwtAccessTokenConverter")
    private JwtAccessTokenConverter jwtAccessTokenConverter;
    @Resource
    private JwtTokenEnhancer jwtTokenEnhancer;

    /**
     * 密码授权模式的配置
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //TokenEnhancerChain是TokenEnhance的一个实现类
        TokenEnhancerChain chain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        delegates.add(jwtAccessTokenConverter);//还要把转换器放进去用来实现jwtTokenEnhancer的互相转换
        chain.setTokenEnhancers(delegates);
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                //可以看到主要是增加了 JwtAccessTokenConverter JWT访问令牌转换器和JwtTokenStore JWT令牌存储组件,
                //通过AuthorizationServerEndpointsConfigurer 授权服务器端点配置加入两个实例
                .tokenStore(tokenStore)
                .accessTokenConverter(jwtAccessTokenConverter)
                .tokenEnhancer(chain); //设置JWT增强内容


    }

    /**
     * 授权配置
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        /*传来的参数clients是我们的应用,要去找授权服务器授权,授权完了之后会给我们授权码,我们
         * (client)拿着授权码再到授权服务器去获取令牌,获取到令牌之后拿着令牌去资源服务器获取资源
         * */

        clients.inMemory() //.inMemory()放入内存。我们为了方便,直接放在内存中生成client,正常情况下是我们主动找授权服务器注册的时候才会有处理。
                .withClient("client") //指定client。参数为唯一client的id
                .secret(passwordEncoder.encode("112233")) //指定密钥
                .redirectUris("http://www.baidu.com") //指定重定向的地址,通过重定向地址拿到授权码。
                //.redirectUris("http://localhost:8081/login") //单点登录到另一服务器
                .accessTokenValiditySeconds(60 * 10) //设置Access Token失效时间
                .refreshTokenValiditySeconds(60 * 60 * 24) //设置refresh token失效时间
                .scopes("all") //指定授权范围
                .autoApprove(true) //自动授权,不需要手动允许了
                /**
                 * 授权类型:
                 * "authorization_code" 授权码模式
                 * "password"密码模式
                 * "refresh_token" 刷新令牌
                 */
                .authorizedGrantTypes("authorization_code", "password", "refresh_token"); //指定授权类型 可以多种授权类型并存。

    }

    /**
     * 单点登录配置
     *
     * @param security
     * @throws Exception
     */
    /*@Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //必须要身份认证,单点登录必须要配置
        security.tokenKeyAccess("isAuthenticated()");
    } */
}
 

复制代码

(2)资源管理器

企业生产环境下授权服务器和资源服务器是两个单独的服务器,我们为了学习,使用了单Model项目,所以放在了一起。

ResourceServerConfig.java

package com.xxxx.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

/**
 * 描述:资源管理器 @EnableResourceServer,extends ResourceServerConfigurerAdapter
 * 为了模拟,授权服务器和资源服务器放在了一起,正常情况是解耦的。
 */
@Configuration
@EnableResourceServer //开启资源服务器
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
         http.requestMatchers().antMatchers("/api/**").and()
                .authorizeRequests()//授权的请求
                //进行接口的鉴权处理
                .antMatchers("/api/user/save").hasAuthority("admin")
                //其余接口不做鉴权,只需要认证即可
                .anyRequest()
                .authenticated();

    }
}
复制代码

(3)JWT内容增强器

JwtTokenEnhancer.java

package com.xxxx.springsecurityoauth2demo.config;

import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;

import java.util.HashMap;
import java.util.Map;

/**
 * 描述:配置JwtTokenEnhancer添加自定义信息 ,继承TokenEnhancer实现一个JWT内容增强器
 */
public class JwtTokenEnhancer implements TokenEnhancer {
    /**
     * JWT内容增强器
     * @param oAuth2AccessToken
     * @param oAuth2Authentication
     * @return
     */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        Map<String, Object> info = new HashMap();
        info.put("enhance", "增强的信息");
        //给的参数是oAuth2的AccessToken,实现类是DefaultOAuth2AccessToken,
        //里面有个setAdditionalInformation方法添加自定义信息(Map类型)
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
        return oAuth2AccessToken;
    }
}

复制代码

(4)TokenStore配置类

JwtTokenStoreConfig.java

package com.xxxx.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * 描述:TokenStore配置类。
 * TokenStore的实现类,有InMemoryTokenStore、JdbcTokenStore、JwtTokenStore、RedisTokenStore。
 * JwtAccessTokenConverter JWT访问令牌转换器和 JwtTokenStore JWT令牌存储组件
 */
@Configuration
public class JwtTokenStoreConfig {
    /**
     * 生成TokenStore来保存token  此处为JwtTokenStore实现
     * @return TokenStore
     */
    @Bean
    public TokenStore jwtTokenStore() {
        //需要传入JwtAccessTokenConverter
        return new JwtTokenStore(jwtAccessTokenConverter());
    }
    /**
     *  生成JwtAccessTokenConverter转换器,并设置密钥
     * @return JwtAccessTokenConverter
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置jwt密钥
        jwtAccessTokenConverter.setSigningKey("test_key");
        return jwtAccessTokenConverter;
    }

    /**
     * JwtTokenEnhancer的注入
     * @return
     */
    @Bean
    public JwtTokenEnhancer jwtTokenEnhancer() {
        return new JwtTokenEnhancer();
    }
}
复制代码

(5)Security核心配置类

SecurityConfig.java

package com.xxxx.springsecurityoauth2demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

/**
 * 描述:Security核心配置类
 * 1.重写configure(HttpSecurity http)
 * 2.配置 PasswordEncoder的Ioc注入。
 */
@Configuration
@EnableWebSecurity //开启Web Security
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //配置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        http.authorizeRequests()
                //放行授权服务器的几个端点请求、登录请求、登出请求。
                .antMatchers("/oauth/**", "/login/**", "/logout/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                //.and() 就相当于回到 http再继续配置
                .and()
                //放行所有的表单请求
                .formLogin()
                .permitAll()
                .and()
                //关闭csrf
                .csrf().disable();

    }

    /**
     * 密码授权模式用到的AuthenticationManager类
     *
     * @return
     * @throws Exception
     */
    @Override
    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
复制代码

7.配置Spring Security的UserDetailService实现从数据库查询信息进行认证

(1)MyUserService接口

package com.xxxx.springsecurityoauth2demo.service;
/**
 * 描述:MyUserService接口
 */
public interface MyUserService{
}
复制代码

(2)MyUserServiceImpl

package com.xxxx.springsecurityoauth2demo.service.impl;


import com.xxxx.springsecurityoauth2demo.model.pojo.SecurityUser;
import com.xxxx.springsecurityoauth2demo.model.pojo.User;
import com.xxxx.springsecurityoauth2demo.service.MyUserService;
import com.xxxx.springsecurityoauth2demo.service.UserService;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 描述:自定义UserDetailsService实现类
 * 名言:越难找的bug往往是越低级的
 */
@Service  //因为没有加Service注解,所以please login  一直报用户名密码错误!!!
public class MyUserServiceImpl implements UserDetailsService, MyUserService {

    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userService.getUserByUserName(username);
        String name = user.getName();
        String password = user.getPassword();
        String authority = user.getAccount();
        return new SecurityUser(name, password, AuthorityUtils.commaSeparatedStringToAuthorityList(authority));
    }
}
复制代码

(3)自定义Security框架的User实体

SecurityUser.java

package com.xxxx.springsecurityoauth2demo.model.pojo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

/**
 * 描述:自定义Security框架的User实体
 */
public class SecurityUser implements UserDetails {

    private String username;
    private String password;
    private List<GrantedAuthority> authorities;

    public SecurityUser(String username, String password, List<GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
复制代码

8.Controller层

(1)针对jwt token测试用的Controller

TestUserController.java

package com.xxxx.springsecurityoauth2demo.controller;


import io.jsonwebtoken.Jwts;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;

/**
 * 描述:UserController  模拟资源服务器用的,用来访问资源的。
 */
@RestController
@RequestMapping("/user")
public class TestUserController {

    //测试用的,不与数据库做连接
    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication, HttpServletRequest request) {
        //Authorization是在请求头中的属性。
        String header = request.getHeader("Authorization");
        //bearer :jwt token,所以bearer加空格后的第七个才是token。
        String token = header.substring(header.lastIndexOf("bearer") + 7);
        return Jwts.parser()    
                .setSigningKey("test_key".getBytes(StandardCharsets.UTF_8))//指定编码格式,要不然token有中文转换异常
                .parseClaimsJws(token)
                .getBody();
    }


}
复制代码

(2)User资源服务器 UserController

UserService.java

package com.xxxx.springsecurityoauth2demo.controller;

import com.xxxx.springsecurityoauth2demo.model.req.ReqUser;
import com.xxxx.springsecurityoauth2demo.service.UserService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * 描述:User资源服务器 UserController
 */
@RestController
@RequestMapping("/api/user")
public class UserController {
    @Resource
    private UserService userService;

    /**
     * 得到所有用户列表,所有权限可以访问
     * @return
     */
    @PostMapping("/users")
    public Object getAllUsers() {
        return userService.getAllUsers();
    }

    /**
     * 增加用户,只有权限为admin的用户才可以访问
     */
    @PostMapping("/save")
    public Object save(@RequestBody ReqUser reqUser) {
        return userService.save(reqUser);
    }
}
复制代码

9.Service层

(1)UserService接口

UserService.java

package com.xxxx.springsecurityoauth2demo.service;

import com.xxxx.springsecurityoauth2demo.model.pojo.User;
import com.xxxx.springsecurityoauth2demo.model.req.ReqUser;

/**
 * 描述:UserService接口
 */
public interface UserService {
    Object getAllUsers();

    User getUserByUserName(String username);

    Object save(ReqUser reqUser);
}
复制代码

UserServiceImpl.java

package com.xxxx.springsecurityoauth2demo.service.impl;

import com.xxxx.springsecurityoauth2demo.model.dao.UserMapper;
import com.xxxx.springsecurityoauth2demo.model.pojo.User;
import com.xxxx.springsecurityoauth2demo.model.req.ReqUser;
import com.xxxx.springsecurityoauth2demo.service.UserService;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * 描述:UserServiceImpl
 */
@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserMapper userMapper;

    @Override
    public Object getAllUsers() {
        List<User> users = userMapper.selectAllUsers();
        return users;
    }

    @Override
    public User getUserByUserName(String username) {
        return userMapper.selectUserByUsername(username);
    }

    @Override
    public Object save(ReqUser reqUser) {
        int count = userMapper.save(reqUser);
        return count;
    }


}

复制代码

10.Req对象

User请求参数对象ReqUser ReqUser.java

package com.xxxx.springsecurityoauth2demo.model.req;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 描述:User请求参数对象
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ReqUser {
    private String account;

    private String description;

    private String password;

    private String name;
}
复制代码

二、postman测试

(1)密码模式

在这里插入图片描述

用户直接输入密码,Client会拿用户名密码直接访问第三方,第三方认证后回执Access Token。多用于同一项目不同端口,比如维新运动需要微信认证,大家都是同一个项目,知道了密码也没什么关系。

需要访问固定接口/oauth/token 输入固定参数 在这里插入图片描述 在这里插入图片描述

拿到token后,凭token可以访问到资源服务器的内容

(1)先访问TestController里的getCurrentUser来解析一下token。(也可以去JWT官网或其他网站进行解析,这里我是自己直接编写了代码解析) 在这里插入图片描述

如果token不正确,则认证失败,禁止访问 在这里插入图片描述

如果token过期,禁止访问

在这里插入图片描述

访问UserController.java接口与数据库进行交互 先访问一个只要认证通过就可以访问的不需要鉴权的接口 在这里插入图片描述

==再访问一个只允许admin权限访问的接口==

成功添加用户。

在这里插入图片描述

换成权限为user的用户访问此接口,则经过鉴权之后拒绝授权访问。 在这里插入图片描述 在这里插入图片描述

(2)授权码模式

需要访问指定地址先获取授权码,再根据授权码获取token 在这里插入图片描述 User-Agent相当于你用第三方登录弹出的登录网页)会带上 客户凭证(client Credentials)和重定向的uri去到要认证服务器里面去,认证服务器会让Resource Owner也就是用户去认证,用户同意了,会返回一个授权码给User-Agent再给Client。客户端Client拿到授权码之后会跟去我们重定向的URI和授权码再次去找到授权服务器,此时认证服务器会根据授权码返回一个Access Token(可以也返回Refresh Toke)。Access Token是必须要有的,Refresh Token是选择性的。

这里用普通权限user去访问需要admin权限的api/save接口 首先获取授权码,访问固定的url获取授权码

访问此链接:

http://localhost:8080/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://www.baidu.com&scope=all
复制代码

这个链接的含义http://localhost:8080的 /oauth/authorize 【固定接口地址】到我们的授权服务器请求,response_type=code去获取授权码 &client_id=client指定client的id 【自己定义的client的id】&redirect_uri【自己要重定向的】重定向地址 &scope=all就是全部范围。

输入上面的url会自动跳转登录页面,道理很简单,授权码模式不会携带用户名和密码,所以需要你登录,这也是授权码模式和密码模式的区别之一了。

在这里插入图片描述

code=授权码 在这里插入图片描述 在这里插入图片描述 接下来我们可有凭借token去访问资源了

(3)refresh_token模式

因为JWT Token是无状态的,不会在服务器端存储,所以我们需要设定Access Token的有效时间,而且时间不能太长,否则安全性会差。(如果你不设置Access Token的有效时间那么毫无安全性可言了)但是Access Token过期太快还要用户重新获取授权码,也就是重新输入用户名密码登录吗?那这样用户体验度也太差了吧?于是就有了refresh_token,当Access_Token过期时,只需要再检验refresh_token便可以重新获得Access_Token。所以一般而言,Access_Token的有效期会很短,而refresh_token的有效期会比较长。

先正常获取

在这里插入图片描述 假设Access_token已经过期了,我们只需要凭借refresh_token便可以拿到新的Access_Token,不需要输入用户名密码等参数了。在这里插入图片描述 在这里插入图片描述

三、整合SSO

1.什么是SSO(Single Sign On)

单点登录全称Single Sign On (以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分

在这里插入图片描述

说人话就是当你在登录百度首页之后,左边红色框内的百度系统旗下加粗样式站点都不需要登录了,实现了一次性鉴别,全网站登录,对用户而言极为友好。

在这里插入图片描述 相比于单系统登录, sso需要 个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息, 其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。

2.项目整合实现SSO

(1)客户端工程结构

在这里插入图片描述

application.properties

server.port=8081
#多客户端不配置Cookie的话会导致cookie的name相同,会出现Cookie冲突,冲突会导致登录验证不通过
server.servlet.session.cookie.name=OAUTH2-CLIENT-SESSIONID01
#授权服务器地址
oauth2-server-url:http://localhost:8080
#与授权服务器对应的配置
security.oauth2.client.client-id=client
security.oauth2.client.client-secret=112233
#获取授权码的地址配置
security.oauth2.client.user-authorization-uri=${oauth2-server-url}/oauth/authorize
#获取Access Token的地址配置
security.oauth2.client.access-token-uri=${oauth2-server-url}/oauth/token
#获取Jwt Token (授权服务器基于Spring Cloud Oauth2创建后,配置TokenStore为JwtTokenStore,访问/oauth/token_key接口获取公钥)
security.oauth2.resource.jwt.key-uri=${oauth2-server-url}/oauth/token_key


复制代码

Controller

Controller.java (简单实现,测试用)

package com.xxxx.oauth2client01demo.controller;

import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 描述:Controller
 */
@RestController
@RequestMapping("/user")
public class Controller {

    @RequestMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication) {
        return authentication;
    }
}

复制代码

项目入口开启SSO注解

在这里插入图片描述

(2)认证服务器端工程结构

我们为了学习和测试,沿用本项目的认证服务器,不再单独创建认证服务器。

指定重定向地址

在这里插入图片描述

单点登录配置

在这里插入图片描述

3.SSO测试

在这里插入图片描述

跳转到认证服务器完成认证之后,自动跳回要访问的接口地址,此时可以访问客户端的资源。 在这里插入图片描述

四、 项目源码

项目源码地址

GitHub: github.com/pleineluna/…

Gitee:gitee.com/tsukuyo98/l…

SSO客户端源码地址:

GitHub: github.com/pleineluna/…

Gitee:gitee.com/tsukuyo98/o…

(顺便求各位点个Star⭐吧)

参考资料:

www.cnblogs.com/xiaofengxzz…

www.bilibili.com/video/BV1Cz…

文章分类
后端