Spring Security OAuth2.0认证授权学习与使用
1.1 什么是认证
简单的例子(粗浅的理解):
1、你去ATM取钱需要密码
2、你去买票需要身份证
3、你去坐高铁需要对应车票
4、你登录QQ,需要输入账号密码
5、认证类似一个物品或*的唯一标识
1.1.1 系统为什么要认证?
认证是为了保护系统的隐私数据与资源,只有用户拥有合法的身份才能访问该系统的资源。
1.1.2 认证
用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时,
系统要求验证用户的身份信息,身法合法方可继续访问,不合法则拒绝访问。
常见的用户身份认证方式有:
1、用户名密码登录认证
2、二维码登录认证
3、手机短信登录认证
4、指纹认证等方式
1.2 什么是会话?
用户认证通过后,为了避免用户每次操作都进行认证,可将用户的信息保存在会话中。
1.2.1 会话
会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等
1.2.2 基于Session的认证方式如下图(根据图中步骤):
他的交互流程是,用户认证成功后,在服务端生成用户相关信息的数据保存在session(当前会话)中,
发给客户端的session_id存放到cookid中,
这样用户客户端请求时带上session_id就可以验证服务器端是否存在session数据,
以此完成用户的合法校验,当前用户推出系统或session过期销毁时,客户端的session_id也就无效了
1.2.3 基于token认证方式如下图(根据图中步骤):
它的交互流程是,基于用户认证成功后,服务端生成一个token发给客户端,
客户端可以放到cookie或localStorage(用来存储客户端临时信息的对象)等存储中,
每次请求时带上token,服务端收到token通过验证后即可确认用户身份。
1.2.4 session和token的区别
基于sess的认证方式由Servlet规范定制,服务端要存储sess信息需要真用内存资源,客户端需要支持cookie;
基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。
如今移动互联网时代更多类型的客户端需要接入系统,系统多此采用的前后端分离的架构进行实现,所以基于token
的方式更适合当下。
1.3 什么是授权?
拿微信来举例子,微信登录成功后用户即可使用微信的功能,
比如:发红包、发朋友圈、添加好友等,没有绑定银行卡的用户是无法发送红包的,绑定银行卡的用户才可以发红包,
发红包功能、发欧阳泉功能都是微信的资源即功能资源,用户拥有发红包功能的权限才可以正常使用发红包功能,
拥有发朋友圈功能的权限才可以使用发朋友圈功能,这个根据用户来控制用户使用资源你的过程是授权。
1.3.1 为什么要授权?
认证是为了保护用户身份的合法性,授权则是为了更烦细粒度的对隐私数据进行划分,授权是在认证通过后发生的
控制不同的用户能够访问不同的资源。
1.3.2 授权
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源你的访问权限则正常访问,没有权限则
拒绝访问。
1.4 授权的数据模型
如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型、
授权可简单理解微Who对What(which)进行How操作,包括如下:
Who,即主体(Subject),主体一般是指用户,也可以是程序,也可以是需要访问系统中的资源等。
What,即资源(Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、
页面、按钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统
订单信息都属于实体资源(数据资源),实体资源有资源类型和资源实例组成,比如商品信息微资源类型,商品编号
为001的商品为资源实例等(可能不好理解,就看做资源是类,实例是你new了一个类)。
How,权限/许可(permission),规定了用户对资源的操作许可,权限离开资源没有意义,如用户查新权限、
用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户对那些资源都有那些
操作许可。
主体、资源、权限关系如下图:
主体、资源、权限相关的数据模型如下:
主体:用户id,账号,密码,…
资源:资源id,资源名称,访问地址,…
权限(分功能资源和数据资源):权限id,权限标识,权限名称,资源id,…
角色:角色id,角色名称,…
角色和权限关系:角色id,权限id,…
主体(用户)和角色的关系:用户id,角色id,…
主体(用户)、资源、权限关系图如下
通常企业开发中将资源和权限表何为一张权限表,如下:
资源:资源id,资源名称,访问地址,…
权限:权限id,权限标识,权限名称,资源id,…
合并为:
权限:权限id,权限标识,权限名称,资源名称,资源访问地址,…
修改后数据模型之间的关系如下图:
1.5 RBAC
如何实现授权?业界通常基于RBAC实现授权。
1.5.1 基于角色的访问控制
RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权
比如:
主体的角色为总经理,可以查询企业运营报表,查询员工工资信息等,访问控制流程如下图:
根据上图中的判断逻辑,授权代码可以表示如下:
if(主体.hasRole("总经理角色id")){
//查询工资
}
如果上图中查询工资所需要的角色变化为总经理和部门经理,此时需要修改判断逻辑为“判断用户的角色是否是总经理或部门经理”,修改代码如下:
if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){
//查询工资
}
缺点:当修改角色的权限是需要修改授权的相关代码,系统的可扩展性差、健壮性差等。
1.5.2 基于资源的访问控制
RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权。
比如:用户必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:
根据上图中的判断,授权代码可以表示为:
if(主体.hasPermission("查询工资权限标识")){
//查询工资
}
优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修改
授权代码,系统可扩展性强,健壮性强。
2 基于Session的认证方式
2.1认证流程(为了看起来方便,又抄了一遍)
基于Session认证方式的流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话),而发
给客户端的session_id存到cookie中,这样用客户端请求时带上session_id就可以验证服务器端是否存在session
数据,以此完成用户的合法校验。当用户推出系统或session过期销毁时,客户端的sessi_id也就无效了。
下图是session认证方法的流程图
基于session的认证机制由Servlet规范定制,Servlet容器已实现,用户通过HttpSession的操作方法即可实现,
如下是HttpSession相关的操作API
方法 | 含义 |
---|---|
HttpSession getSession(Boolean create) | 获取当前HttpSession对象 |
void setAttribute(String name,Object value) | 想session中存放对象 |
Object getAttribute(String name) | 从session中获取对象 |
void removeAttrbute(String name) | 一移除session中对象 |
void invalidate() | 是HttpSession失效 |
略… |
2.2 创建工程(springMvc,如需SpringBoot请下翻)
本案例工程使用maven进行构建,使用SpringMVC、Servlet3.0实现。
2.2.1 创建maven工程
创建maven工程security-springMvc,工程结构如下
使用Tomcat7-maven-plugin插件来运行工程
<?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.webinar.sercurity</groupId>
<artifactId>sercurity-springMvc</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
<build>
<finalName>sercurity-springMvc</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<version>2.2</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<encoding>utf-8</encoding>
<useDefaultDelimiters>true</useDefaultDelimiters>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>
**/*
</include>
</includes>
</resource>
<resource>
<directory>src/main/java</directory>
<includes>
<include>
**/*.xml
</include>
</includes>
</resource>
</resources>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
2.2.2 spirng容器配置
在config包下定义ApplicationConfig,它对应web.xml中ContextLoaderListener的配置
package com.webinar.security.springMvc.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
/**
* @Author Webinar
* @ClassName ApplicationConfig
* @Description TODO
* @Date 2022/10/8 21:45
* @Version 1.0
*/
@Configuration
@ComponentScan(basePackages = "com.webinar.security.springMvc"
, excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, value = Controller.class)})
public class ApplicationConfig {
//在此配置除了Controller的其他bean,比如:数据库连接池、失误管理器、业务bean等
}
2.2.3 servletContext配置
创建WEB-INF/view/login.jsp(当前登录页面为空白页面)
package com.webinar.security.springMvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
/**
* @Author Webinar
* @ClassName WebConfig
* @Description TODO
* @Date 2022/10/8 21:55
* @Version 1.0
*/
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.webinar.security.springMvc",
includeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,value = Controller.class)})
public class WebConfig implements WebMvcConfigurer {
//视图解析器
@Bean
public InternalResourceViewResolver viewResolver(){
InternalResourceViewResolver viewResolver=new InternalResourceViewResolver();
//配置视图前缀
viewResolver.setPrefix("/WEB-INF/view/");
//配置视图后缀
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
2.2.4 加载Spring容器
在init包下定义Spring容器初始化类SpringApplicationInitializer,此类实现WebApplicationInitializer
接口,Spring容器启动是加载WebApplicationInitializer接口的所有实现类。
package com.webinar.security.springMvc.init;
import com.webinar.security.springMvc.config.ApplicationConfig;
import com.webinar.security.springMvc.config.WebConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
/**
* @Author Webinar
* @ClassName SpringApplicationInitializer
* @Description TODO
* @Date 2022/10/8 22:05
* @Version 1.0
*/
public class SpringApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
//Spring容器 相当于加载applicationContext.xml
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{ApplicationConfig.class};
}
//servletContext 相当于加载springMvc,xml
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
//url-mapping
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
2.3 实现认证功能
2.3.1 认证页面
在webapp/WEB-INF/view下定义认证页面login.jsp,本案例只是测试认证流程,页面没有添加css样式,页面实现
可填入用户名,密码,出发登录将提交表单信息至/login,内容如下:
<%--
Created by IntelliJ IDEA.
User: Webinar
Date: 2022/10/8
Time: 22:00
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>用户登录</title>
</head>
<body>
<form action="login" method="post">
用户名:<input type="text" name="username"><br>
密 码:<input type="password" name="password"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
在WebConfig中新增如下配置,将/直接导向login.jsp页面:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("login");
}
配置Mven启动项
启动过程可能会出现报错,原因是Tomcat7版本太低 不支持lombok 这些先不管 点击下面第一个红款为启动地址
点击地址后会弹出奇丑无比的登录页面
2.3.2 认证接口
创建service包以及AuthenticationService接口 以及Impl包和AuthenticationServiceImpl实现类 用于认证login服务 和model包 创建 userDto和AuthenticationRrquest类 用于接收参数和返回数据
1、service
package com.webinar.security.springMvc.service;
import com.webinar.security.springMvc.model.AuthenticationRrquest;
import com.webinar.security.springMvc.model.UserDto;
/**
* @Author Webinar
* @ClassName AuthenticationService
* @Description TODO
* @Date 2022/10/8 22:25
* @Version 1.0
*/
public interface AuthenticationService {
/**
*
* @param authenticationRrquest 用户认证请求,账号密码等等
* @return 认证成功的用户信息
*/
UserDto authentication(AuthenticationRrquest authenticationRrquest);
}
package com.webinar.security.springMvc.service.Impl;
import com.webinar.security.springMvc.model.AuthenticationRrquest;
import com.webinar.security.springMvc.model.UserDto;
import com.webinar.security.springMvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
/**
* @Author Webinar
* @ClassName AuthenticationServiceImpl
* @Description TODO
* @Date 2022/10/8 22:31
* @Version 1.0
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
/**
* 校验用户认证身份信息
* @param authenticationRrquest 用户认证请求,账号密码等等
* @return
*/
@Override
public UserDto authentication(AuthenticationRrquest authenticationRrquest) {
return null;
}
}
2、model
package com.webinar.security.springMvc.model;
import lombok.Data;
/**
* @Author Webinar
* @ClassName UserDto
* @Description TODO
* @Date 2022/10/8 22:28
* @Version 1.0
*/
@Data
@AllArgsConstructor
public class UserDto {
//用户认证信息
private String id;
private String username;
private String password;
private String fullName;
private String mobile;
}
package com.webinar.security.springMvc.model;
import lombok.Data;
/**
* @Author Webinar
* @ClassName AuthenticationRrquest
* @Description TODO
* @Date 2022/10/8 22:28
* @Version 1.0
*/
@Data
public class AuthenticationRrquest {
//认证请求参数
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
创建包以及实现类后 编写认证login服务代码
用户进入认证页面,输入账号密码,点击登录,请求/login进行身份认证。
(1)定义认证接口,此接口用于对传来的用户名账号密码进行校验,若成功则返回该用户的详细信息,否则抛出错误异常(如下)(AuthenticationServiceImpl实现类中):
package com.webinar.security.springMvc.service.Impl;
import com.webinar.security.springMvc.model.AuthenticationRrquest;
import com.webinar.security.springMvc.model.UserDto;
import com.webinar.security.springMvc.service.AuthenticationService;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* @Author Webinar
* @ClassName AuthenticationServiceImpl
* @Description TODO
* @Date 2022/10/8 22:31
* @Version 1.0
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
/**
* 校验用户认证身份信息
* @param authenticationRrquest 用户认证请求,账号密码等等
* @return
*/
@Override
public UserDto authentication(AuthenticationRrquest authenticationRrquest) {
//1、校验参数是否为空
if(authenticationRrquest==null ||
authenticationRrquest.getUsername().isEmpty() ||
authenticationRrquest.getUsername().isEmpty()){
throw new RuntimeException("账号或密码为空");
}
//2、根据账号去查询数据库(由于这里是测试程序,则采用模拟数据)
UserDto user = getUserDtoByUserName(authenticationRrquest.getUsername());
//校验账户是否存在
if(user == null){
throw new RuntimeException("账户不存在");
}
//校验密码
if(!authenticationRrquest.getPassword().equals(user.getPassword())){
throw new RuntimeException("账号或密码错误");
}
//认证通过 返回用户的身份信息
return user;
}
//根据账号查询用户信息
private UserDto getUserDtoByUserName(String userName){
return userMap().get(userName);
}
//用户信息
private Map<String,UserDto> userMap(){
Map<String,UserDto> stringUserDtoMap=new HashMap<>();
stringUserDtoMap.put("zhangsan",new UserDto("1010","zhangsan","123","1231321","123"));
stringUserDtoMap.put("lisi",new UserDto("1011","lisi","456","42456456","213"));
return stringUserDtoMap;
}
}
创建Controller包 创建LoginController
package com.webinar.security.springMvc.Controller;
import com.webinar.security.springMvc.model.AuthenticationRrquest;
import com.webinar.security.springMvc.model.UserDto;
import com.webinar.security.springMvc.service.AuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author Webinar
* @ClassName LoginController
* @Description TODO
* @Date 2022/10/8 22:49
* @Version 1.0
*/
@RestController
public class LoginController {
@Autowired
AuthenticationService authenticationService;
@RequestMapping(value = "/login",produces = "text/plain;charset=utf-8")
public String login(AuthenticationRrquest authenticationRrquest){
UserDto authentication = authenticationService.authentication(authenticationRrquest);
return authentication.toString() +"----"+"登录成功!";
}
}
效果
总结:以上是实现登录的全过程,而Security也是利用这样的流程来进行封装的,所需需要了解一下。