SpringSecurity最全实战讲解一|8月更文挑战

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

一、基本概念

认证

用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹认证等方式。

系统为什么要认证?

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

怎么进行认证?

授权

授权是用户认证通过后,根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。

为什么要授权?

认证是为了保证用户身份的合法性,授权则是为了更细粒度的对隐私数据进行划分,授权是在认证通过后发生的,

控制不同的用户能够访问不同的资源。

会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

RBAC模型

​ 主体 -》 角色 -》 资源 -》行为

如何设计一个权限系统?
在这里插入图片描述

二、一个自己实现的权限模型 BasicAuth:

下面我们自己实现一个基于Session方式的RBAC模型的项目。

先创建一个maven父工程AuthDemo,管理maven版本。

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tuling</groupId>
    <artifactId>AuthDemo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <java.version>1.8</java.version>
        <spring-boot-version>2.3.3.RELEASE</spring-boot-version>
        <spring-cloud-version>Greenwich.RELEASE</spring-cloud-version>
    </properties>

    <modules>
        <module>basicAuth</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>javax.interceptor</groupId>
                <artifactId>javax.interceptor-api</artifactId>
                <version>1.2</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.47</version>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>5.1.47</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security</groupId>
                <artifactId>spring-security-jwt</artifactId>
                <version>1.1.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.security.oauth.boot</groupId>
                <artifactId>spring-security-oauth2-autoconfigure</artifactId>
                <version>2.1.2.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>
复制代码

注:目前我们需要使用到的就是spring-boot-dependencies。其他依赖包含了后面几个部分需要的依赖版本,在这里一次全部引入。

然后我们创建一个basicAuth的子工程。子工程是采用SpringBoot方式快速搭建的伪前后端分离的项目。

项目整体机构如下:
在这里插入图片描述
pom依赖非常简单,只需要引入spring-boot-starter 和 spring-boot-starter-web两个依赖。

<?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>AuthDemo</artifactId>
        <groupId>com.tuling</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>basicAuth</artifactId>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot-version}</version>
                <configuration>
                    <mainClass>com.tuling.BasicAuthApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
复制代码

然后创建启动类

package com.tuling.basicAuth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BasicApplication {

    public static void main(String[] args) {
        SpringApplication.run(BasicApplication.class,args);
    }
}
复制代码

以及springboot的配置文件 application.properties,我们只简单定义下接口

server.port=8080
复制代码

然后我们开始创建基于RBAC模型的三个关键实体

UserBean:

package com.tuling.basicAuth.bean;

import java.util.ArrayList;
import java.util.List;

public class UserBean {

    private String userId;
    private String userName;
    private String userPass;
    private List<RoleBean> userRoles = new ArrayList<>();
    private List<ResourceBean> resourceBeans = new ArrayList<>();

    public UserBean(){

    }
    public UserBean(String userId, String userName, String userPass) {
        this.userId = userId;
        this.userName = userName;
        this.userPass = userPass;
    }
   ...getter and setter...
    public boolean havaPermission(String resource) {
        return this.resourceBeans.stream()
                .filter(resourceBean -> resourceBean.getResourceName().equals(resource))
                .count()>0;
    }
}

复制代码

RoleBean:

package com.tuling.basicAuth.bean;

import java.util.List;

public class RoleBean {

    private String roleId;
    private String roleName;
    private List<ResourceBean> resources;

    public RoleBean(){

    }
    
    public RoleBean(String roleId, String roleName) {
        this.roleId = roleId;
        this.roleName = roleName;
    }

    ... getter and setter ...
}

复制代码

ResourceBean:

package com.tuling.basicAuth.bean;

/**
 * Spring Security中,资源被简化成一个字符串。
 * 而在自己设计资源时,可以设计不同类型的资源控制不同的行为。
 * 例如 菜单资源,Rest接口资源,页面控件资源等。
 */
public class ResourceBean {

    private String resourceId;
    private String resourceType;
    private String resourceName;

    public ResourceBean(){

    }

    public ResourceBean(String resourceId, String resourceName) {
        this.resourceId = resourceId;
        this.resourceName = resourceName;
    }
... getter and setter ...
}

复制代码

然后我们定义三个Controller,其中MobileController和SalaryController就是需要控制权限的访问资源,LoginController就是登陆的入口。

MobileController:

package com.tuling.basicAuth.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/mobile")
public class MobileController {

    @GetMapping("/query")
    public String query(){
        return "mobile";
    }
}

复制代码

SalaryController:

package com.tuling.basicAuth.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/salary")
public class SalaryController {

    @GetMapping("/query")
    public String query(){
        return "salary";
    }
}

复制代码

LoginController:

package com.tuling.basicAuth.controller;

import com.tuling.basicAuth.bean.UserBean;
import com.tuling.basicAuth.service.AuthService;
import com.tuling.basicAuth.util.MyConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
@RequestMapping("/common/")
public class LoginController {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private AuthService authService;

    @PostMapping("/login")
    public UserBean login(UserBean loginUser, HttpServletRequest request){
        UserBean user = authService.userLogin(loginUser);
        if(null != user){
            logger.info("user login succeed");
            request.getSession().setAttribute(MyConstants.FLAG_CURRENTUSER,user);
        }
        logger.info("user login failed");
        return user;
    }

    @PostMapping("/getCurrentUser")
    public Object getCurrentUser(HttpSession session){
        return session.getAttribute(MyConstants.FLAG_CURRENTUSER);
    }

    @PostMapping("/logout")
    public void logout(HttpSession session){
        session.removeAttribute(MyConstants.FLAG_CURRENTUSER);
    }
}


复制代码

LoginController中依赖AuthService,来对登陆进行认证。

AuthService:

package com.tuling.basicAuth.service;

import com.tuling.basicAuth.bean.UserBean;
import com.tuling.basicAuth.util.TestData;
import org.springframework.stereotype.Service;

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

@Service
public class AuthService {

    private final String demoUserName = "admin";
    private final String demoUserPass = "admin";

    @Resource
    private TestData testData;

    public UserBean userLogin(UserBean user){
        UserBean queryUser = testData.qeryUser(user);
        if(null != queryUser){
            queryUser.setUserId(UUID.randomUUID().toString());
        }
        return queryUser;
    }
}

复制代码

然后AuthService中依赖testData作为模拟的用户数据来源。由于是演示,就不从数据库加载了。

TestData:

package com.tuling.basicAuth.util;

import com.tuling.basicAuth.bean.ResourceBean;
import com.tuling.basicAuth.bean.RoleBean;
import com.tuling.basicAuth.bean.UserBean;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Component
public class TestData {

    private List<UserBean> allUser;

    /**
     * 模拟数据库获取到的数据。
     * admin用户 拥有admin角色,拥有mobile和salary两个资源。
     * mobile用户,拥有mobile角色,拥有mobile资源。
     * worker用户,拥有worker角色,没有资源。
     * @return
     */
    private List<UserBean> getAllUser(){
        if(null == allUser){
            allUser = new ArrayList<>();

            ResourceBean mobileResource = new ResourceBean("1","mobile");
            ResourceBean salaryResource = new ResourceBean("2","salary");
            List<ResourceBean> adminResources = new ArrayList<>();
            adminResources.add(mobileResource);
            adminResources.add(salaryResource);

            List<ResourceBean> managerResources = new ArrayList<>();
            managerResources.add(salaryResource);

            RoleBean adminRole = new RoleBean("1","mobile");
            adminRole.setResources(adminResources);
            RoleBean managerRole = new RoleBean("2","salary");
            managerRole.setResources(managerResources);
            List<RoleBean> adminRoles = new ArrayList<>();
            adminRoles.add(adminRole);
            List<RoleBean> managerRoles = new ArrayList<>();
            managerRoles.add(managerRole);

            UserBean user1 = new UserBean("1","admin","admin");
            user1.setUserRoles(adminRoles);
            user1.setResourceBeans(adminResources);
            UserBean user2 = new UserBean("2","manager","manager");
            user2.setUserRoles(managerRoles);
            user2.setResourceBeans(managerResources);
            UserBean user3 = new UserBean("3","worker","worker");

            allUser.add(user1);
            allUser.add(user2);
            allUser.add(user3);
        }
        return allUser;
    }

    public UserBean qeryUser(UserBean user){
        List<UserBean> allUser = this.getAllUser();
        List<UserBean> userList = allUser.stream().filter(userBean ->
                userBean.getUserName().equals(user.getUserPass())
                        && userBean.getUserPass().equals(user.getUserPass())
        ).collect(Collectors.toList());

        return userList.size()>0?userList.get(0):null;
    }
}

复制代码

​ 然后,还定义了一个常量类 MyConstants:

package com.tuling.basicAuth.util;

public class MyConstants {

    public static final String FLAG_CURRENTUSER = "currnetUser";

    public static final String RESOURCE_COMMON = "common";
    public static final String RESOURCE_MOBILE = "mobile";
    public static final String RESOURCE_SALARY = "salary";
}

复制代码

然后,在static目录下有两个简单的页面index.html 登录页面和main.html登录后的主页面,引入jquery做简单的逻辑控制。前端不是我们的重点, 那就先直接复制下。

到这里呢。我们的这个SpringBoot工程就可以启动了。 启动后可以直接访问前端的两个页面,也是可以完成登录的。而且,登录后主页面上的两个按钮是可以随登录用户不同而部分隐藏的。但是,虽然页面上把访问按钮给隐藏了,我们还是可以通过直接访问后台接口来获取没有权限的资源。那后面我们就要添加后台的权限控制。

首先我们注入一个配置器WebMvcConfigurer,来对SpringBoot进行部分配置。

package com.tuling.basicAuth.config;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;

@Component
public class MyWebAppConfigurer implements WebMvcConfigurer {

    @Resource
    private AuthInterceptor authInterceptor;
    //配置权限拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authInterceptor).addPathPatterns("/**");
    }
    //简单配置启动页面
    @Override
    public void addViewControllers(ViewControllerRegistry registry)
    {
        registry.addViewController("/").setViewName("redirect:/index.html");
    }
}

复制代码

其中这个AuthInterceptor,就是以拦截器的形式来实现权限管控。

package com.tuling.basicAuth.config;

import com.tuling.basicAuth.bean.UserBean;
import com.tuling.basicAuth.util.MyConstants;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、不需要登录就可以访问的路径
        String requestURI = request.getRequestURI();
        if (requestURI.contains(".") || requestURI.startsWith("/"+ MyConstants.RESOURCE_COMMON+"/")) {
            return true;
        }
        //2、未登录用户,直接拒绝访问
        if (null == request.getSession().getAttribute(MyConstants.FLAG_CURRENTUSER)) {
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write("please login first");
            return false;
        } else {
            UserBean currentUser = (UserBean) request.getSession().getAttribute(MyConstants.FLAG_CURRENTUSER);
            //3、已登录用户,判断是否有资源访问权限
            if (requestURI.startsWith("/"+MyConstants.RESOURCE_MOBILE+"/")
                    && currentUser.havaPermission(MyConstants.RESOURCE_MOBILE)) {
                return true;
            } else if (requestURI.startsWith("/"+MyConstants.RESOURCE_SALARY+"/")
                    && currentUser.havaPermission(MyConstants.RESOURCE_SALARY)) {
                return true;
            } else {
                response.setCharacterEncoding("UTF-8");
                response.getWriter().write("no auth to visit");
                return false;
            }
        }
    }
}

复制代码

这样我们的整个系统就完成了。

这其中,我们定义了三个用户, admin, manager ,worker 。有两个资源mobile(查看员工手机号) , salary(查看薪水)。

​ 其中mobile资源就对应main.html上的 查看手机号 按钮,以及对应的访问地址 http://localhost:8080/mobile/query。 而salary资源则对应main.html上的 查看薪水 http://localhost:8080/salary/query 这就是需要控制的行为。

​ 然后我们给admin赋予了两个资源,manager有salary资源,而worker未赋予任何资源。可以查看登录后的页面按钮以及后台查询地址的访问效果。

演示完我们自己的RBAC权限模型后,我们来体验下Spring Security如何让这个流程变得更健壮、优雅。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享