这是我参与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如何让这个流程变得更健壮、优雅。