spring-cloud-oauth2+jwt+vue前后端分离的权限管理系统

设计理念

  1. 权限框架选择:一般springboot类型项目的权限系统shiro或者spring-security都能满足。现在越来越多的系统都是前后端分离,且后端很多是springcloud框架,无疑oauth2是个更好的选择。
  2. 前端预期效果:所有前端请求都经过网关再调取后台服务,如果用户未登录或者登陆过期返回401,跳转至登陆页面,登陆成功拿到后端返回的access_token存在localStorage,未授权的请求返回403提示未授权,其他不需要授权的页面或者请求可以直接访问,前端axios请求加拦截器,请求时从localStorage获取token,将token添加到请求头中。
  3. 后端逻辑处理:每个前端请求当成是一个资源,一个资源是否可以访问由当前用户是否拥有可以访问该资源的权限决定。一个用户对应多个角色,一个角色对应多个权限,一个资源对应多个权限。
  4. 数据库设计:t_user(用户表)、t_role(角色表)、t_authority(权限表)、t_resource(资源表)、t_user_role(用户角色表)、t_role_auhority(角色权限表)、t_resource_authority(资源权限表)

技术栈

后端

  1. springcloud:后端框架
  2. nacos:注册跟配置
  3. oauth2:权限(密码模式)
  4. spring-security:权限配合使用
  5. jwt:access_token转化格式
  6. gateway:网关springcloud-gateway
  7. mybatis-plus:持久层框架,mybatis加强版
  8. mysql:数据库
  9. redisson:token存储,redis的加强版

前端

  1. vue

项目结构

后端

说明

  1. cloud-parent:父工程
    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>
    <packaging>pom</packaging>
    <modules>
        <module>cloud-auth</module>
        <module>cloud-gateway</module>
        <module>cloud-api</module>
        <module>common-web</module>
        <module>cloud-system</module>
    </modules>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.spring.cloud</groupId>
    <artifactId>cloud-parent</artifactId>
    <version>0.0.1</version>
    <name>cloud-parent</name>
    <description>Demo project for Spring Cloud</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR9</spring-cloud.version>
    </properties>

    <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>
        </plugins>
    </build>

</project>

  1. cloud-api:简单web工程(为了测试权限)
  2. cloud-auth:认证服务,用户认证,token存储等
    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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-parent</artifactId>
        <groupId>com.spring.cloud</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-auth</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
            <groupId>com.nimbusds</groupId>
            <artifactId>nimbus-jose-jwt</artifactId>
            <version>8.16</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.13.4</version>
        </dependency>

    </dependencies>

</project>

application.yml

server:
  port: 88

spring:
  application:
    name: cloud-auth
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848

management:
  endpoints:
    web:
      exposure:
        include: "*"

logging:
  level:
    com.spring.cloud.auth.mapper: debug

KeyPairController

@RestController
public class KeyPairController {

    private final KeyPair keyPair;

    public KeyPairController(KeyPair keyPair) {
        this.keyPair = keyPair;
    }

    @GetMapping("/rsa/publicKey")
    public Map<String, Object> getKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }
}

Oauth2AuthorizationAdapter :权限适配器

@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationAdapter extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private UserServiceImpl userDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtAccessTokenConverter accessTokenConverter;

    @Autowired
    private JdbcClientDetailsService clientDetailsService;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //客户端详情:数据库方式
        clients.withClientDetails(clientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                //密码模式要带authenticationManager参数
                .authenticationManager(authenticationManager)
                //自定义userDetailsService
                .userDetailsService(userDetailsService)
                //配置令牌存储策略
                .tokenStore(tokenStore)
                //token转换器
                .accessTokenConverter(accessTokenConverter);

    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        //允许表单认证
        security.allowFormAuthenticationForClients();
    }
}

WebSecurityConfig :WebSecurityConfig 配置类

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers("/rsa/publicKey").permitAll()
                .anyRequest()
                .authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public TokenStore tokenStore(RedisConnectionFactory factory) {
        return new RedisTokenStore(factory);
    }

    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService(DataSource dataSource) {
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter(KeyPair keyPair) {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setKeyPair(keyPair);
        return jwtAccessTokenConverter;
    }

    @Bean
    public KeyPair keyPair() {
        //从classpath下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserDetails superAdmin(PasswordEncoder passwordEncoder, UserMapper userMapper) {
        List<GrantedAuthority> authorities = new ArrayList<>();
        List<String> userAuthorities = userMapper.getUserAuthorities(null);
        userAuthorities.forEach(authority -> authorities.add(new SimpleGrantedAuthority(authority)));
        return new User("spring_cloud_oauth2", passwordEncoder.encode("123456"), authorities);
    }
}

UserServiceImpl :UserDetailsService 数据库实现方式

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserDetails superAdmin;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //默认设置了一个拥有所有权限的用户
        if(superAdmin.getUsername().equals(username)){
            return superAdmin;
        }
        LambdaQueryWrapper<UserEntity> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(UserEntity::getUsername, username);
        UserEntity userEntity = userMapper.selectOne(wrapper);
        if (userEntity == null) {
            throw new InvalidGrantException("用户名或密码错误");
        }
        List<String> userAuthorities = userMapper.getUserAuthorities(userEntity.getId());
        List<GrantedAuthority> authorities = new ArrayList<>();
        userAuthorities.forEach(authority -> authorities.add(new SimpleGrantedAuthority(authority)));
        return new User(username, passwordEncoder.encode(userEntity.getPassword()), authorities);
    }

}

UserMapper

public interface UserMapper extends BaseMapper<UserEntity> {

    List<String> getUserAuthorities(Integer userId);
    
}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring.cloud.auth.mapper.UserMapper">

    <select id="getUserAuthorities" resultType="java.lang.String">
        SELECT DISTINCT a.authority_name
        FROM t_user u
        LEFT JOIN t_user_role ur ON ur.user_id = u.id
        LEFT JOIN t_role r ON r.id = ur.role_id
        LEFT JOIN t_role_authority ra ON ra.role_id = r.id
        LEFT JOIN t_authority a ON a.id = ra.authority_id
        WHERE a.authority_name IS NOT NULL
        <if test="userId != null">
            AND u.id = #{userId}
        </if>
    </select>

</mapper>
  1. cloud-gateway:资源服务,权限校验
    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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-parent</artifactId>
        <groupId>com.spring.cloud</groupId>
        <version>0.0.1</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-gateway</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-resource-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-oauth2-jose</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

application.yml

server:
  port: 90

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/cloud-auth?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: root
  application:
    name: cloud-gateway
  cloud:
    nacos:
      server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: api #唯一标识
          uri: lb://cloud-api #转发的地址,写服务名称
          predicates:
            - Path=/api/** #判断匹配条件,即地址带有/api/**的请求,会转发至lb:cloud-api
          filters:
            - StripPrefix=1
        - id: auth
          uri: lb://cloud-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: system
          uri: lb://cloud-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:88/rsa/publicKey' #配置RSA的公钥访问地址

logging:
  level:
    com.spring.cloud.gateway.mapper: debug

ResourceServerConfig:资源服务配置文件

@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
                                                            Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter,
                                                            GatewayReactiveAuthorizationManager manager) {
        http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter);
        http.authorizeExchange()
                //.pathMatchers().permitAll()的方式不能动态授权白名单,改为manager手动授权
                .anyExchange()
                .access(manager)
                .and().exceptionHandling()
                .and().csrf().disable();
        return http.build();
    }

}

GatewayReactiveAuthorizationManager :权限管理器

@Configuration
public class GatewayReactiveAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

    private final static String AUTHORITY_PREFIX = "ROLE_";

    @Autowired
    private ResourceMapper resourceMapper;

    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        String url = authorizationContext.getExchange().getRequest().getURI().getPath();
        String method = authorizationContext.getExchange().getRequest().getMethodValue();
        //根据请求方式和url获取当前资源
        ResourceEntity resourceEntity = resourceMapper.getResourceEntity(method, url);
        if (resourceEntity != null) {
            //如果该资源permitAll,直接返回true
            if (resourceEntity.getPermitAll() != null && resourceEntity.getPermitAll() == 1) {
                return Mono.just(new AuthorizationDecision(true));
            }
            List<String> authorities = new ArrayList<>();
            //获取该资源能够访问的权限
            List<String> accessAuthorities = resourceMapper.getAccessAuthorities(resourceEntity.getId());
            if (!CollectionUtils.isEmpty(accessAuthorities)) {
                for (String authority : accessAuthorities) {
                    authorities.add(AUTHORITY_PREFIX + authority);
                }
            }
            return mono
                    //用户是否已经认证,没有返回401(token没有或者失效)
                    .filter(Authentication::isAuthenticated)
                    //获取用户的权限
                    .flatMapIterable(Authentication::getAuthorities)
                    .map(GrantedAuthority::getAuthority)
                    //资源权限跟用户拥有的权限匹配,有一个满足即可
                    .any(authorities::contains)
                    .map(AuthorizationDecision::new)
                    //为空返回false
                    .defaultIfEmpty(new AuthorizationDecision(false));
        }
        //为空返回false,即403(forbidden)
        return Mono.just(new AuthorizationDecision(false));
    }

    @Bean
    public Converter<Jwt, Mono<AbstractAuthenticationToken>> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities");
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
}

ResourceMapper

public interface ResourceMapper extends BaseMapper<ResourceEntity> {

    /**
     * Description 如果一个url跟method匹配到多个会报错
     *
     * @param method 请求方式
     * @param url    url
     * @author qinchao
     * @date 2021/4/28 14:30
     * @return com.spring.cloud.gateway.entity.ResourceEntity
     */
    ResourceEntity getResourceEntity(String method, String url);

    /**
     * Description 获取资源所能访问的权限
     *
     * @param resourceId
     * @author qinchao
     * @date 2021/4/28 14:31
     * @return java.util.List<java.lang.String>
     */
    List<String> getAccessAuthorities(Integer resourceId);

}

ResourceMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spring.cloud.gateway.mapper.ResourceMapper">

    <select id="getResourceEntity" resultType="com.spring.cloud.gateway.entity.ResourceEntity">
        SELECT r.*
        FROM t_resource r
        WHERE r.method = #{method}
          AND #{url} regexp concat(r.module,r.url)
    </select>

    <select id="getAccessAuthorities" resultType="string">
        SELECT a.`authority_name`
        FROM t_resource_authority ra
                 LEFT JOIN t_authority a ON a.id = ra.authority_id
        WHERE ra.resource_id = #{resourceId}
    </select>

</mapper>
  1. cloud-system:权限管理服务,用户、角色、权限、资源的增删改查
  2. common:web通用工程,常用工具配置

前端
在这里插入图片描述

数据库说明

在这里插入图片描述

  1. oauth_client_details:oauth2的数据库实现方式
  2. t_user、t_role、t_authority:用户角色权限及其关联关系表比较简单就不赘述了(去掉一层role,即直接用户权限关系也是可以的)
  3. t_resource:资源表(重点)
    resource_type:该资源是页面还是ajax,module:请求所属模块,url:请求地址,method:请求方式,permit_all:该资源是否直接开放(比如登陆页和登陆接口为1,直接放行)

前端页面及操作

  1. 登陆页面:直接放行;登陆接口直接放行
  2. Hello页面:直接放行
  3. api页面:测试、超管、系统管理员可访问;里面3个按钮,hello直接放行,test测试能访问,admin超管能访问(以上3个页面及其按钮操作已经可以测试权限效果了)
  4. 系统管理:对用户、角色、权限、资源的增删改查,系统管理员跟超管能访问(对应的后台服务cloud-system)
  5. 退出:localStorage删除token,跳转登陆页即可,后端不用任何处理

最终效果

在这里插入图片描述
当访问未授权的页面获取请求时
在这里插入图片描述
当未登陆或者登陆过期时
在这里插入图片描述

项目地址

后端:cloud-oauth2
前端

  • 2
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
SpringCloud是一款开源的微服务框架,OAuth2是一种授权框架,Vue是一个流行的前框架,ElementUI是一套基于Vue开发的UI框架。结合这些技术栈进行前后分离的快速上手项目实战开发,可以提高开发效率和代码的可维护性。 实践中,可以按照以下步骤进行快速上手项目开发: 1. 搭建后服务:使用SpringCloud搭建微服务架构,并引入Spring Security和OAuth2来实现认证和授权功能,确保后接口的安全性。 2. 配置OAuth2服务:在后服务中配置OAuth2的服务,定义认证服务器和资源服务器,配置客户信息,如客户ID、客户密钥等。 3. 开发前界面:使用Vue构建前界面,并引入ElementUI来快速搭建页面和组件。利用Vue的组件化开发方式,可以更高效地开发各种交互功能。 4. 实现登录认证:在前界面中使用OAuth2的授权码模式来实现用户登录认证功能,通过向认证服务器发送请求来获取访问令牌,并将令牌保存到前的Cookie或localStorage中。 5. 发起请求并解析响应:在前界面中使用Axios库来发起HTTP请求,并在请求头中携带访问令牌,后服务器根据令牌进行权限验证。前收到响应后解析数据,并进行相应的操作。 6. 实现权限控制:根据后接口的权限设定,在前界面中进行权限控制,隐藏或禁用没有权限的功能。可以通过在请求头中携带用户的角色信息,与后进行验证。 7. 编写测试用例:保证代码的质量和功能的稳定性,编写相应的测试用例来进行单元测试和接口测试,确保项目的正确运行。 通过以上步骤,可以快速上手并实战开发SpringCloud、OAuth2、Vue和ElementUI结合的前后分离项目。不仅可以提高开发效率,还能保证项目的安全性和可维护性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值