Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
相对于 Shiro,在 SSM/SSH 中整合 Spring Security 都是比较麻烦的操作,所以,Spring Security 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有 Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。
自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了 自动化配置方案,可以零配置使用 Spring Security。
因此,一般来说,常见的安全管理技术栈的组合是这样的:
注意,这只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行的
spring security 的核心功能主要包括:
官网:https://projects.spring.io/spring-security/
源代码: https://github.com/spring-projects/spring-security/
Shiro
首先Shiro较之 Spring Security,Shiro在保持强大功能的同时,还在简单性和灵活性方面拥有巨大优势。
Shiro是一个强大而灵活的开源安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。如下是它所具有的特点:
Spring Security
除了不能脱离Spring,shiro的功能它都有。而且Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。Spring Security的权限细粒度更高(笔者还未发现高在哪里)。
注:
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
OpenID 系统的第一部分是身份验证,即如何通过 URI 来认证用户身份。目前的网站都是依靠用户名和密码来登录认证,这就意味着大家在每个网站都需要注册用户名和密码,即便你使用的是同样的密码。如果使用 OpenID ,你的网站地址(URI)就是你的用户名,而你的密码安全的存储在一个 OpenID 服务网站上(你可以自己建立一个 OpenID 服务网站,也可以选择一个可信任的 OpenID 服务网站来完成注册)。
与OpenID同属性的身份识别服务商还有ⅥeID,ClaimID,CardSpace,Rapleaf,Trufina ID Card等,其中ⅥeID通用账户的应用最为广泛。
综述
个人认为现阶段需求,权限的操作粒度能控制在路径及按钮上,数据粒度通过sql实现。Shrio简单够用。
至于OAuth,OpenID 站点间统一登录功能,现租户与各个产品间单点登录已经通过cookies实现,所以Spring Security的这两个功能可以不考虑。
SpringSide网站的权限也是用Shrio做的。
shiro有很多地方都比spring security方便简单直接,比起spring security的庞大模式更容易理解和切入一些,而spring security比shiro功能上要多一点,再就是和spring框架的无缝对接,比如支持spel等,有时候比shiro更方便灵活。不过spring security的很多源代码我看了感觉可插拔性设计的不够优化,想自己扩展的话要做很多无谓的工作。
(参考文档)https://blog.csdn.net/I_am_Hutengfei/article/details/100561564?ops_request_misc=%7B%22request%5Fid%22%3A%22160799550219195271652910%22%2C%22scm%22%3A%2220140713.130102334.pc%5Fall.%22%7D&request_id=160799550219195271652910&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_v2~rank_v29-1-100561564.nonecase&utm_term=springboot +springSecurity实现前后端分离登录认证及权限控制&spm=1018.2118.3001.4449
package com.Guo.response;
/**
* @Author: NieChangan
* @Description: 返回码定义
* 规定:
* #200表示成功
* #1001~1999 区间表示参数错误
* #2001~2999 区间表示用户错误
* #3001~3999 区间表示接口异常
* #后面对什么的操作自己在这里注明就行了
*/
public enum ResultCode implements CustomizeResultCode {
/* 成功 */
SUCCESS(200, "成功"),
/* 默认失败 */
COMMON_FAIL(999, "失败"),
/* 参数错误:1000~1999 */
PARAM_NOT_VALID(1001, "参数无效"),
PARAM_IS_BLANK(1002, "参数为空"),
PARAM_TYPE_ERROR(1003, "参数类型错误"),
PARAM_NOT_COMPLETE(1004, "参数缺失"),
/* 用户错误 */
USER_NOT_LOGIN(2001, "用户未登录"),
USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
USER_CREDENTIALS_ERROR(2003, "密码错误"),
USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
USER_ACCOUNT_DISABLE(2005, "账号不可用"),
USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),
UPDATE_ERROR_EXCEPTION(2010, "修改失败"),
DELETE_ERROR_EXCEPTION(2011, "删除用户信息失败"),
/*部门错误*/
DEPARTMENT_NOT_EXIST(3007, "部门不存在"),
DEPARTMENT_ALREADY_EXIST(3008, "部门已存在"),
/* 业务错误 */
NO_PERMISSION(3001, "没有权限"),
/*运行时异常*/
ARITHMETIC_EXCEPTION(9001,"算数异常");
private Integer code;
private String message;
ResultCode(Integer code,String message){
this.code=code;
this.message=message;
}
@Override
public Integer getCode() {
return code;
}
@Override
public String getMessage() {
return message;
}
}
package com.Guo.response;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
* @author Admin
* @project Result
* @description
* @date 2020-11-30-13:27
*/
@Data
public class Result {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String,Object> data = new HashMap<>();
/**
* 构造方法私有化,里面的方法都是静态方法
* 达到保护属性的作用
*/
private Result(){
}
/**
* 这里是使用链式编程
*/
public static Result ok(){
Result result = new Result();
result.setSuccess(true);
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage(ResultCode.SUCCESS.getMessage());
return result;
}
public static Result error(ResultCode resultCode){
Result result = new Result();
result.setSuccess(false);
result.setCode(resultCode.getCode());
result.setMessage(resultCode.getMessage());
return result;
}
public static Result error1(){
Result result = new Result();
result.setSuccess(false);
result.setCode(ResultCode.COMMON_FAIL.getCode());
result.setMessage(ResultCode.COMMON_FAIL.getMessage());
return result;
}
/**
* 自定义返回成功与否
* @param success
* @return
*/
public Result success(Boolean success){
this.setSuccess(success);
return this;
}
public Result message(String message){
this.setMessage(message);
return this;
}
public Result code(Integer code){
this.setCode(code);
return this;
}
public Result data(String key, Object value){
this.data.put(key,value);
return this;
}
public Result data(Map<String,Object> map){
this.setData(map);
return this;
}
}
package com.Guo.response;
/**
* @author Admin
* @project CustomizeResultCode
* @description
* @date 2020-11-30-13:19
*/
public interface CustomizeResultCode {
/**
* 获取错误状态码
* @return 错误状态码
*/
Integer getCode();
/**
* 获取错误信息
* @return 错误信息
*/
String getMessage();
}
package com.Guo.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author Admin
* @project JSONAuthentication
* @description 封装输出json格式的类
* @date 2020-12-15-14:50
*/
public abstract class JSONAuthentication {
/**
* 输出json
* @param response
* @param request
* @param data
* @throws IOException
* @throws ServletException
*/
protected void WriteJSON(HttpServletResponse response,
HttpServletRequest request,
Object data) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Method", "POST,GET");
//输出json
PrintWriter out = response.getWriter();
//writeValueAsString(data)将value值以json返回
out.write(new ObjectMapper().writeValueAsString(data));
out.flush();
out.close();
}
}
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.8.BUILD-SNAPSHOTversion>
<relativePath/>
parent>
<groupId>com.GuogroupId>
<artifactId>springsecurity02artifactId>
<version>0.0.1-SNAPSHOTversion>
<name>springsecurity02name>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger-uiartifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>io.springfoxgroupId>
<artifactId>springfox-swagger2artifactId>
<version>2.7.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-generatorartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.11version>
dependency>
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocity-engine-coreartifactId>
<version>2.2version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
<repositories>
<repository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
repository>
<repository>
<id>spring-snapshotsid>
<name>Spring Snapshotsname>
<url>https://repo.spring.io/snapshoturl>
<snapshots>
<enabled>trueenabled>
snapshots>
repository>
repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestonesid>
<name>Spring Milestonesname>
<url>https://repo.spring.io/milestoneurl>
pluginRepository>
<pluginRepository>
<id>spring-snapshotsid>
<name>Spring Snapshotsname>
<url>https://repo.spring.io/snapshoturl>
<snapshots>
<enabled>trueenabled>
snapshots>
pluginRepository>
pluginRepositories>
project>
spring:
application:
name: springsecurity02
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=CTT
username: root
password: root
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/*.xml
type-aliases-package: com.Guo.entity
jwt:
secret: xinguan
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`permission_code` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限code',
`permission_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限名',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '权限表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, 'create_user', '创建用户');
INSERT INTO `sys_permission` VALUES (2, 'query_user', '查看用户');
INSERT INTO `sys_permission` VALUES (3, 'delete_user', '删除用户');
INSERT INTO `sys_permission` VALUES (4, 'modify_user', '修改用户');
-- ----------------------------
-- Table structure for sys_request_path
-- ----------------------------
DROP TABLE IF EXISTS `sys_request_path`;
CREATE TABLE `sys_request_path` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`url` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求路径',
`description` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '路径描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '请求路径' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_request_path
-- ----------------------------
INSERT INTO `sys_request_path` VALUES (1, '/getUser', '查询用户');
INSERT INTO `sys_request_path` VALUES (2, '/deleteUser', '删除用户');
INSERT INTO `sys_request_path` VALUES (3, '/addUser', '新增用户');
INSERT INTO `sys_request_path` VALUES (4, '/updateUser', '修改用户');
-- ----------------------------
-- Table structure for sys_request_path_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_request_path_permission_relation`;
CREATE TABLE `sys_request_path_permission_relation` (
`id` int(11) NULL DEFAULT NULL COMMENT '主键id',
`url_id` int(11) NULL DEFAULT NULL COMMENT '请求路径id',
`permission_id` int(11) NULL DEFAULT NULL COMMENT '权限id'
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '路径权限关联表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_request_path_permission_relation
-- ----------------------------
INSERT INTO `sys_request_path_permission_relation` VALUES (NULL, 1, 2);
INSERT INTO `sys_request_path_permission_relation` VALUES (NULL, 2, 3);
INSERT INTO `sys_request_path_permission_relation` VALUES (NULL, 3, 1);
INSERT INTO `sys_request_path_permission_relation` VALUES (NULL, 4, 4);
INSERT INTO `sys_request_path_permission_relation` VALUES (NULL, 1, 1);
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role_code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色code',
`role_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名',
`role_description` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色说明',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'admin', '管理员', '管理员,拥有所有权限');
INSERT INTO `sys_role` VALUES (2, 'user', '普通用户', '普通用户,拥有部分权限');
-- ----------------------------
-- Table structure for sys_role_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission_relation`;
CREATE TABLE `sys_role_permission_relation` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`role_id` int(11) NULL DEFAULT NULL COMMENT '角色id',
`permission_id` int(11) NULL DEFAULT NULL COMMENT '权限id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色-权限关联关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_role_permission_relation
-- ----------------------------
INSERT INTO `sys_role_permission_relation` VALUES (1, 1, 1);
INSERT INTO `sys_role_permission_relation` VALUES (2, 1, 2);
INSERT INTO `sys_role_permission_relation` VALUES (3, 1, 3);
INSERT INTO `sys_role_permission_relation` VALUES (4, 1, 4);
INSERT INTO `sys_role_permission_relation` VALUES (7, 2, 2);
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号',
`user_name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '用户密码',
`last_login_time` datetime(0) NULL DEFAULT NULL COMMENT '上一次登录时间',
`enabled` tinyint(1) NULL DEFAULT 1 COMMENT '账号是否可用。默认为1(可用)',
`not_expired` tinyint(1) NULL DEFAULT 1 COMMENT '是否过期。默认为1(没有过期)',
`account_not_locked` tinyint(1) NULL DEFAULT 1 COMMENT '账号是否锁定。默认为1(没有锁定)',
`credentials_not_expired` tinyint(1) NULL DEFAULT 1 COMMENT '证书(密码)是否过期。默认为1(没有过期)',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
`create_user` int(11) NULL DEFAULT NULL COMMENT '创建人',
`update_user` int(11) NULL DEFAULT NULL COMMENT '修改人',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'user1', '用户1', '$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i', '2020-12-18 14:47:40', 1, 1, 1, 1, '2019-08-29 06:28:36', '2020-12-18 14:47:40', 1, 1);
INSERT INTO `sys_user` VALUES (2, 'user2', '用户2', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', '2020-12-18 12:01:02', 1, 1, 1, 1, '2019-08-29 06:29:24', '2020-12-18 12:01:02', 1, 2);
-- ----------------------------
-- Table structure for sys_user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role_relation`;
CREATE TABLE `sys_user_role_relation` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`user_id` int(11) NULL DEFAULT NULL COMMENT '用户id',
`role_id` int(11) NULL DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户角色关联关系表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of sys_user_role_relation
-- ----------------------------
INSERT INTO `sys_user_role_relation` VALUES (1, 1, 1);
INSERT INTO `sys_user_role_relation` VALUES (2, 2, 2);
SET FOREIGN_KEY_CHECKS = 1;
创建WebSecurityConfig继承WebSecurityConfigurerAdapter类,并实现configure(AuthenticationManagerBuilder auth)和 configure(HttpSecurity http)方法。后续我们会在里面加入一系列配置,包括配置认证方式、登入登出、异常处理、会话管理等
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式等
super.configure(auth);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http相关的配置,包括登入登出、异常处理、会话管理等
super.configure(http);
}
}
这是实现自定义用户认证的核心逻辑,loadUserByUsername(String username)的参数就是登录时提交的用户名,返回类型是一个叫UserDetails 的接口,需要在这里构造出他的一个实现类User,这是Spring security提供的用户信息实体
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//需要构造出 org.springframework.security.core.userdetails.User 对象并返回
return null;
}
}
这里我们使用他的一个参数比较详细的构造函数,源码如下
User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities)
其中参数:
String username:用户名
String password: 密码
boolean enabled: 账号是否可用
boolean accountNonExpired:账号是否过期
boolean credentialsNonExpired:密码是否过期
boolean accountNonLocked:账号是否锁定
Collection<? extends GrantedAuthority> authorities):用户权限列表
这就与我们的创建的用户表的字段对应起来了,Spring security都为我们封装好了,如果用户信息的状态异常,登录时则会抛出相应的异常,根据捕获到的异常判断是什么原因(账号过期/密码过期/账号锁定等等…),进而就可以提示前台了。
我们就按照该参数列表构造出我们所需要的数据,然后返回,就完成了基于JDBC的自定义用户认证。
首先用户名密码以及用户状态信息都是从用户表里进行单表查询来的,而权限列表则是通过用户表、角色表以及权限表等关联查出来的,那么接下来就是准备service和dao层方法了
映射文件
UserMapper
package com.Guo.mapper;
import com.Guo.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface UserMapper extends BaseMapper<User> {
/**
* 根据用户名查询用户
*
* @param account
* @return
*/
User selectByName(String account);
}
PermossionMapper
package com.Guo.mapper;
import com.Guo.entity.Permission;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 查询用户的权限列表
*
* @param userId
* @return
*/
List<Permission> selectListByUser(Integer userId);
/**
* 获取请求路径的权限列表
* @param requestPath
* @return
*/
List<Permission> selectListByPath(@Param("requestPath") String requestPath);
}
<!--根据用户名查询用户-->
<select id="selectByName" resultType="User">
select * from sys_user where account= #{account};
</select>
<mapper namespace="com.Guo.mapper.PermissionMapper">
<select id="selectListByUser" resultType="Permission">
SELECT p.*
FROM sys_user AS u
LEFT JOIN sys_user_role_relation AS ur
ON u.id = ur.user_id
LEFT JOIN sys_role AS r
ON r.id = ur.role_id
LEFT JOIN sys_role_permission_relation AS rp
ON r.id = rp.role_id
LEFT JOIN sys_permission AS p
ON p.id = rp.permission_id
WHERE u.id = #{userId}
select>
<select id="selectListByPath" resultType="com.Guo.entity.Permission">
SELECT p.*
FROM sys_user AS u
LEFT JOIN sys_user_role_relation AS ur
ON u.id = ur.user_id
LEFT JOIN sys_role AS r
ON r.id = ur.role_id
LEFT JOIN sys_role_permission_relation AS rp
ON r.id = rp.role_id
LEFT JOIN sys_permission AS p
ON p.id = rp.permission_id
LEFT JOIN sys_request_path_permission_relation AS requestPathAndPermision
ON rp.permission_id = requestPathAndPermision.permission_id
LEFT JOIN sys_request_path AS requestPath
ON requestPath.id = requestPathAndPermision.url_id
<where>
<if test="requestPath != null">
requestPath.url = #{requestPath}
if>
where>
GROUP BY p.id
select>
mapper>
/**
* 根据用户名查询用户
*
* @param userName
* @return
*/
User selectByName(String userName);
/**
* 查询用户的权限列表
*
* @param userId
* @return
*/
List<Permission> selectListByUser(Integer userId);
package com.Guo.service;
import com.Guo.entity.Permission;
import com.Guo.entity.User;
import com.baomidou.mybatisplus.extension.service.IService;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* @author Admin
* @project PermissionMapper
* @description
* @date 2020-12-15-11:20
*/
public interface PermissionService extends IService<Permission> {
/**
* 查询用户的权限列表
*
* @param userId
* @return
*/
List<Permission> selectListByUser(Integer userId);
/**
* 获取请求路径的权限列表
* @param requestPath
* @return
*/
List<Permission> selectListByPath(@Param("requestPath") String requestPath);
}
package com.Guo.service.impl;
import com.Guo.entity.Permission;
import com.Guo.mapper.PermissionMapper;
import com.Guo.service.PermissionService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author Admin
* @project PermissionServiceimpl
* @description
* @date 2020-12-16-22:15
*/
@Service
public class PermissionServiceimpl extends ServiceImpl<PermissionMapper, Permission> implements PermissionService {
@Override
public List<Permission> selectListByUser(Integer userId) {
return this.baseMapper.selectListByUser(userId);
}
@Override
public List<Permission> selectListByPath(String requestPath) {
return this.baseMapper.selectListByPath(requestPath);
}
}
这样的话流程我们就理清楚了,首先根据用户名查出对应用户,再拿得到的用户的用户id去查询它所拥有的的权限列表,最后构造出我们需要的org.springframework.security.core.userdetails.User对象。
接下来改造一下刚刚自定义的UserDetailsService
package com.Guo.service.impl;
import com.Guo.entity.Permission;
import com.Guo.entity.User;
import com.Guo.mapper.PermissionMapper;
import com.Guo.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @author Admin
* @project UserDetailsServiceImpl
* @description
* @date 2020-12-15-10:38
*/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String account) throws UsernameNotFoundException {
//需要构造出 org.springframework.security.core.userdetails.User 对象并返回
//Granted允许 Authority权限 SimpleGrantedAuthority为GrantedAuthority的实现类
List<GrantedAuthority> authorities = new ArrayList<>();
User user = userMapper.selectByName(account);
if (user == null) {
throw new UsernameNotFoundException(String.format("用户%s不存在", account));
} else {
List<Permission> permissions = permissionMapper.selectListByUser(user.getId());
for (Permission permission : permissions) {
//这里的条件是放 权限code 如create_user
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission.getPermissionCode());
authorities.add(simpleGrantedAuthority);
}
}
// String username:用户名
// String password: 密码
// boolean enabled: 账号是否可用
// boolean accountNonExpired:账号是否过期 not_expired
// boolean credentialsNonExpired:密码是否过期 credentials_not_expired
// boolean accountNonLocked:账号是否锁定 account_not_locked
// Collection extends GrantedAuthority> authorities):用户权限列
// String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities
return new org.springframework.security.core.userdetails.User
(user.getAccount(),
user.getPassword(),
user.getEnabled(),
user.getNotExpired(),
user.getCredentialsNotExpired(),
user.getAccountNotLocked(), authorities);
}
}
然后将我们的自定义的基于JDBC的用户认证在之前创建的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中声明一下,到此自定义的基于JDBC的用户认证就完成了
@Bean
public UserDetailsService userDetailsService() {
//获取用户账号密码及权限信息
return new UserDetailsServiceImpl();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式
auth.userDetailsService(userDetailsService());
}
新版本的Spring security规定必须设置一个默认的加密方式,不允许使用明文。这个加密方式是用于在登录时验证密码、注册时需要用到。
我们可以自己选择一种加密方式,Spring security为我们提供了多种加密方式,我们这里使用一种强hash方式进行加密。
在WebSecurityConfig 中注入(注入即可,不用声明使用),这样就会对提交的密码进行加密处理了
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式(强hash方式加密)
return new BCryptPasswordEncoder();
}
在演示登录之前我们先编写一个查询接口"/getUser",并将"/getUser"接口规定为需要拥有"query_user"权限的用户可以访问,并在角色-权限关联关系表中给user1用户所属角色(role_id = 1)添加权限"query_user"
然后规定接口"/getUser"只能是拥有"query_user"权限的用户可以访问。后面我们基本都用这个查询接口作为演示,就叫它"资源接口"吧
//接口"/getUser"只能是拥有"query_user"权限的用户可以访问
http.authorizeRequests().
antMatchers("/getUser").hasAuthority("query_user").
演示登录时,如果用户没有登录去请求资源接口就会提示未登录
在前后端不分离的时候当用户未登录去访问资源时Spring security会重定向到默认的登录页面,返回的是一串html标签,这一串html标签其实就是登录页面的提交表单。如图所示
而在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽重定向的登录页面,并返回统一的json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint并在WebSecurityConfig中注入,然后在configure(HttpSecurity http)方法中。AuthenticationEntryPoint主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)
package com.Guo.auth;
import com.Guo.response.Result;
import com.Guo.response.ResultCode;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Admin
* @project MyAuthenticationEntryPoint
* @description
* 表示用户未登录或者已经过期的处理
* 用户尚未登录时处理 身份校验 如token错误
* @date 2020-12-15-14:45
*/
@Component("authenticationEntryPoint")
public class MyAuthenticationEntryPoint extends JSONAuthentication implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
Result error = Result.error(ResultCode.USER_NOT_LOGIN);
this.WriteJSON(response,request,error);
}
}
在WebSecurityConfig中的configure(HttpSecurity http)方法中声明
//异常处理(权限拒绝、登录失效等)
and().exceptionHandling().
authenticationEntryPoint(authenticationEntryPoint).//匿名用户访问无权限资源时的异常处理
再次请求资源接口
前台拿到这个错误时就可以做一些处理了,主要是退出到登录页面
对于登入登出我们都不需要自己编写controller接口,Spring Security为我们封装好了。默认登入路径:/login,登出路径:/logout。 当登录成功或登录失败都需要返回统一的json返回体给前台,前台才能知道对应的做什么处理
package com.Guo.auth;
import com.Guo.jwt.JwtTokenUtil;
import com.Guo.mapper.UserMapper;
import com.Guo.response.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/**
* @author Admin
* @project MyAuthenticationSuccessHandler
* @description
* @date 2020-12-15-15:14
*/
@Component("authenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler {
@Autowired
private UserMapper userMapper;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//将登录成功的授权信息置于springsecurity中(jwt)
SecurityContextHolder.getContext().setAuthentication(authentication);
//更新用户表上次登录时间、更新人、更新时间等字段
User userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
com.Guo.entity.User user = userMapper.selectByName(userDetails.getUsername());
user.setLastLoginTime(new Date());
user.setUpdateTime(new Date());
user.setUpdateUser(user.getId());
userMapper.updateById(user);
//生成token(jwt)
String token = jwtTokenUtil.generateToken(userDetails);
Result message = Result.ok().message("登录成功").data("token",token);
this.WriteJSON(response,request,message);
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// SecurityContextHolder.getContext().setAuthentication(authentication);
// String token = jwtTokenUtil.generateToken(userDetails);
// renderToken(httpServletResponse, token);
}
}
package com.Guo.auth;
import com.Guo.response.Result;
import com.Guo.response.ResultCode;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Admin
* @project MyAuthenticationFailureHandler
* @description 登录失败
* @date 2020-12-15-20:29
*/
@Component("authenticationFailureHandler")
public class MyAuthenticationFailureHandler extends JSONAuthentication implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
//对返回的e进行判断 判断到底是什么原因导致的登录失败
Result result=null;
if (e instanceof AccountExpiredException) {
//账号过期
result = Result.error(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (e instanceof BadCredentialsException) {
//密码错误
result = Result.error(ResultCode.USER_CREDENTIALS_ERROR);
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = Result.error(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (e instanceof DisabledException) {
//账号不可用
result = Result.error(ResultCode.USER_ACCOUNT_DISABLE);
} else if (e instanceof LockedException) {
//账号锁定
result = Result.error(ResultCode.USER_ACCOUNT_LOCKED);
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = Result.error(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else{
//其他错误
result = Result.error(ResultCode.COMMON_FAIL);
}
this.WriteJSON(response,request,result);
//中文乱码在JSONAuthentication类中已经解决
}
}
同样的登出也要将登出成功时结果返回给前台,并且登出之后进行将cookie失效或删除
package com.Guo.auth;
import com.Guo.response.Result;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Admin
* @project MyLogoutSuccessHandler
* @description 登出成功处理器(注销)
* @date 2020-12-15-20:38
*/
@Component("logoutSuccessHandler")
public class MyLogoutSuccessHandler extends JSONAuthentication implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//登出成功其实是需要销毁token的
this.WriteJSON(response,request,Result.ok().message("注销成功"));
}
}
//登入
http.formLogin().permitAll()
//登录成功时替换为我们自定义登录成功的处理器
.successHandler(authenticationSuccessHandler)
//登录失败时替换为我们自定义登录失败的处理器
.failureHandler(authenticationFailureHandler);
//登出
http.logout().permitAll()
//注销成功的处理器
.logoutSuccessHandler(logoutSuccessHandler)
//登出之后删除cookie;
//JSESSIONID(jsessionid)是一个Cookie,
//可以通过在URL后面加上“;jsessionid=xxx”来传递“session id”;
//其中Servlet容器用来记录用户session,当我们创建回话时会自动创建,用来记录用户的访问记录
.deleteCookies("JSESSIONID");
package com.Guo.auth;
import com.Guo.response.Result;
import com.Guo.response.ResultCode;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Admin
* @project MySessionInformationExpiredStrategy
* @description 会话信息过期策略
* @date 2020-12-16-14:05
*/
@Component("sessionAuthenticationStrategy")
public class MySessionInformationExpiredStrategy extends JSONAuthentication implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
//USER_ACCOUNT_USE_BY_OTHERS 账号下线
//第一次在postman里面登录 可以访问getUser
//用户重新在在线的post请求中访问 不可以访问getUser 显示用户已下线
//用户下线是指:一个用户在a处登录 另一个用户在b处登录 导致用户在a处不可以访问getUser方法
Result error = Result.error(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
HttpServletRequest request = sessionInformationExpiredEvent.getRequest();
HttpServletResponse response = sessionInformationExpiredEvent.getResponse();
this.WriteJSON(response,request,error);
}
}
//限制同一账号只能一个用户使用(自己用无效果)
http.sessionManagement().maximumSessions(1)
//用户被挤下线
.expiredSessionStrategy(sessionAuthenticationStrategy);
在之前的章节中我们配置了一个
antMatchers("/getUser").hasAuthority("query_user")
其实我们就已经实现了一个所谓的基于RBAC的权限控制,只不过我们是在WebSecurityConfig中写死的,但是在平时开发中,难道我们每增加一个需要访问权限控制的资源我们都要修改一下WebSecurityConfig增加一个antMatchers(…)吗,肯定是不合理的。因此我们现在要做的就是将需要权限控制的资源配到数据库中.
我们需要实现一个AccessDecisionManager(访问决策管理器),在里面我们对当前请求的资源进行权限判断,判断当前登录用户是否拥有该权限,如果有就放行,如果没有就抛出一个"权限不足"的异常。不过在实现AccessDecisionManager之前我们还需要做一件事,那就是拦截到当前的请求,并根据请求路径从数据库中查出当前资源路径需要哪些权限才能访问,然后将查出的需要的权限列表交给AccessDecisionManager去处理后续逻辑。那就是需要先实现一个SecurityMetadataSource,翻译过来是"安全元数据源",我们这里使用他的一个子类FilterInvocationSecurityMetadataSource。
在自定义的SecurityMetadataSource编写好之后,我们还要编写一个拦截器,增加到Spring security默认的拦截器链中,以达到拦截的目的。
同样的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中然后声明
package com.Guo.auth;
import com.Guo.entity.Permission;
import com.Guo.service.PermissionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/**
* @author Admin
* @project MyFilterInvocationSecurityMetadataSource
* @description
* @date 2020-12-16-21:19
*/
@Component("filterInvocationSecurityMetadataSource")
public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
PermissionService PermissionService;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取请求地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
//查询具体某个接口的权限
//请求路径
//根据请求路径从数据库中查出当前资源路径需要哪些权限才能访问
List<Permission> permissionList = PermissionService.selectListByPath(requestUrl);
if(permissionList == null || permissionList.size() == 0){
//请求路径没有配置权限,表明该请求接口可以任意访问
return null;
}
String[] attributes = new String[permissionList.size()];
for(int i = 0;i<permissionList.size();i++){
attributes[i] = permissionList.get(i).getPermissionCode();
}
return SecurityConfig.createList(attributes);
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
package com.Guo.auth;
import com.sun.net.httpserver.HttpExchange;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
/**
* @author Admin
* @project MyAbstractSecurityInterceptor
* @description 权限拦截器
* @date 2020-12-16-21:05
*/
@Component("abstractSecurityInterceptor")
public class MyAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.filterInvocationSecurityMetadataSource;
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//执行下一个拦截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
}
package com.Guo.auth;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Iterator;
/**
* @author Admin
* @project MyAccessDecisionManager
* @description 角色访问决策器
* @date 2020-12-16-15:09
*/
@Component("accessDecisionManager")
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException {
//获取通过路径 获取该路径的权限集合
Iterator<ConfigAttribute> iterator = collection.iterator();
while (iterator.hasNext()) {
ConfigAttribute ca = iterator.next();
//当前请求需要的权限 一个个循环获取集合中的权限
String needRole = ca.getAttribute();
//当前用户所具有的权限 获取当前登录用户的权限
Collection extends GrantedAuthority> authorities = authentication.getAuthorities();
//将路径权限集合与用户权限集合进行匹配,如果登录用户权限中,没有该请求路径所需要的的权限,就返回权限不足
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足!");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
http.authorizeRequests().
withObjectPostProcessor(new ObjectPostProcessor() {
@Override
public O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器 o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);//安全元数据源
return o;
}
});
http.addFilterBefore((Filter) abstractSecurityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中
参考文档https://www.jianshu.com/p/5b9f1f4de88d
方法如下:
1.使用gloableSession,session是可以复制的,一旦一个服务器登录成功之后,会将得到的session往所有的服务器都复制一份,当服务器特别多的时候,这种复制效率特别低,占用了过多的服务器资源
2.将登录之后的session,存在数据库中,key--value
session.add("user",user);
只需要提供一个key,往数据库中查询就可以了
缺点:1.多增加一台额外的数据库服务器
2.key过期的时候,没办法实现完全同步,续约问题
(续约:当你登录淘宝的时候,设置了30分钟没使用自动下线,但是如果你在这个30分钟之内进行了操作,过期时间往后推30分钟)
3.JWT(JSON WEB TOKEN)
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
第一部分:header 头部 解析这是一个什么类型的token 算法 然后进行base64加密
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
第二部分:playload 载荷
iss: jwt签发者 是由谁生成的
sub: jwt所面向的用户 用来存放的用户名称(用户信息的存放地点)
aud: 接收jwt的一方 是由谁去接收的
exp: jwt的过期时间,这个过期时间必须要大于签发时间 token的失效时间(默认15天)
nbf: 定义在什么时间之前,该jwt都是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
第三部分:signature 签证相当于密钥
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
当点击登录操作时,会到第一个拦截器UsernamePasswordAuthenticationFilter
的doFilter
方法,我们直接看这个类:
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
//判断你的用户名和密码是不是username和password
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = "username";
private String passwordParameter = "password";
private boolean postOnly = true;
//重点走这个方法
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 登录请求必须是POST 请求 如果不是,则会抛出异常
//这里要注意:登录一定要为post请求 其他请求均为get请求
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
// 获取用户名,密码
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
//去空格
username = username.trim();
//根据用户名和密码 生成 Token
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
// 进行验证
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
如果有token则默认是不需要进行认证的,如果没有token,则需要进行userDetailService的认证,通过其中的loaduserbyname方法去进行登录认证,认证如果成功,则通过jwt生成一个token,生成的token在springsecurity的successHandler中以json的方式进行返回到前端,前端会把json进行一个本地存储,以后客户端所做的每一个请求都需要携带token,服务端拿到token会对token进行解析,获取用户名和密码,从而判断该用户是否登录
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
jwt:
secret: xinguan
package com.Guo.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author Admin
* @project JwtTokenUtil
* @description 生成令牌,验证等等一些操作
* @date 2020-12-17-16:38
*/
@Data
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtil {
private String secret;
/**
* 过期时间 毫秒 设置默认1周的过期时间
*/
private static final Long EXCEPTION_TIME=3600000L*24*7;
private String header="Authorization";
/**
* 从数据声明生成令牌
* @param claims 数据声明
* @return 令牌
*/
private String generateToken1(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXCEPTION_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, secret).compact();
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
private Claims getClaimsFromToken(String token) {
Claims claims;
try {
claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}
return claims;
}
/**
* 生成令牌
* @param userDetails 用户
* @return 令牌
*/
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>(2);
//把用户名和签发时间封装在claims
claims.put(Claims.SUBJECT, userDetails.getUsername());
claims.put(Claims.ISSUED_AT, new Date());
return generateToken1(claims);
}
/**
* 从令牌中获取用户名
* @param token 令牌
* @return 用户名
*/
public String getUsernameFromToken(String token) {
String username;
try {
Claims claims = getClaimsFromToken(token);
username = claims.getSubject();
} catch (Exception e) {
username = null;
}
return username;
}
/**
* 判断令牌是否过期
*
* @param token 令牌
* @return 是否过期
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* 刷新令牌
*
* @param token 原令牌
* @return 新令牌
*/
public String refreshToken(String token) {
String refreshedToken;
try {
Claims claims = getClaimsFromToken(token);
claims.put(Claims.ISSUED_AT, new Date());
refreshedToken = generateToken1(claims);
} catch (Exception e) {
refreshedToken = null;
}
return refreshedToken;
}
/**
* 验证令牌
* @param token 令牌
* @param userDetails 用户
* @return 是否有效
*/
public Boolean validateToken(String token, UserDetails userDetails) {
UserDetails user = userDetails;
String username = getUsernameFromToken(token);
return (username.equals(user.getUsername()) && !isTokenExpired(token));
}
}
package com.Guo.Filter;
import com.Guo.jwt.JwtTokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author Admin
* @project JwtAuthenticationTokenFilter
* @description
* @date 2020-12-18-10:31
*/
@Component("oncePerRequestFilter")
class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String headertoken = request.getHeader(jwtTokenUtil.getHeader());
System.out.println("headerToken=" + headertoken);
System.out.println("request getMethod=" + request.getMethod());
//如果不为空,这说明已经登录过了,无需重新登录
if (!StringUtils.isEmpty(headertoken)) {
//postman测试时,自动加入的前缀,要去掉
String token = headertoken.replace("Bearer", "").trim();
System.out.println("token=" + token);
//判断令牌是否过期,默认是一周
// 比较好的解决方案是:
//登录成功获得token后,将token存储到数据库(redis)
// 将数据库版本的token设置过期时间关15~30分钟
//如果数据库中的token版本过期,重新刷新获取新的token
// 注意:刷新获得新token是在token过期时间内有效。
//如果token本身的过期(1周),强制登录,生成新token。
boolean check=false;
try {
check = this.jwtTokenUtil.isTokenExpired(token);
}catch (Exception e){
new Throwable("令牌已过期,请重新登录。"+e.getMessage());
}
if (!check){
//通过令牌获取用户名称
String username = jwtTokenUtil.getUsernameFromToken(token);
System.out.println(username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {
// 将用户信息存入 authentication,方便后续校验
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 将 authentication 存入 ThreadLocal,方便后续获取用户信息
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
}
chain.doFilter(request, response);
}
}
package com.Guo.auth;
import com.Guo.jwt.JwtTokenUtil;
import com.Guo.mapper.UserMapper;
import com.Guo.response.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
/**
* @author Admin
* @project MyAuthenticationSuccessHandler
* @description
* @date 2020-12-15-15:14
*/
@Component("authenticationSuccessHandler")
public class MyAuthenticationSuccessHandler extends JSONAuthentication implements AuthenticationSuccessHandler {
@Autowired
private UserMapper userMapper;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//将登录成功的授权信息置于springsecurity中
SecurityContextHolder.getContext().setAuthentication(authentication);
//更新用户表上次登录时间、更新人、更新时间等字段
User userDetails = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
com.Guo.entity.User user = userMapper.selectByName(userDetails.getUsername());
user.setLastLoginTime(new Date());
user.setUpdateTime(new Date());
user.setUpdateUser(user.getId());
userMapper.updateById(user);
//生成token
String token = jwtTokenUtil.generateToken(userDetails);
Result message = Result.ok().message("登录成功").data("token",token);
this.WriteJSON(response,request,message);
// UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// SecurityContextHolder.getContext().setAuthentication(authentication);
// String token = jwtTokenUtil.generateToken(userDetails);
// renderToken(httpServletResponse, token);
}
}
package com.Guo.auth;
import org.apache.catalina.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.Filter;
/**
* @author Admin
* @project WebSecurityConfig
* @description
* @date 2020-11-18-9:42
*/
//WebSecurityConfig为 Spring Security 的配置类
@Configuration //标识该类是配置类
@EnableWebSecurity //开启 Security 服务
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启全局 Securtiy 注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 设置密码的加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 修改密码的加密模式
*/
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 自定义的认证逻辑
*/
@Autowired
private UserDetailsService userDetailsService;
/**
* 用户尚未登录的处理方案
* 出现异常应该如何处理
*/
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
/**
* 登录成功逻辑处理
*/
@Autowired
private AuthenticationSuccessHandler authenticationSuccessHandler;
/**
* 注销,登出成功逻辑处理
*/
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
/**
* 登录失败逻辑处理
*/
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 模拟用户被挤下线的逻辑处理
*/
@Autowired
private SessionInformationExpiredStrategy sessionAuthenticationStrategy;
@Autowired
private AccessDecisionManager accessDecisionManager;
@Autowired
private AbstractSecurityInterceptor abstractSecurityInterceptor;
@Autowired
private FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource;
@Autowired
private OncePerRequestFilter oncePerRequestFilter;
/**
* 重写认证方法
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//springSecurity拥有自带的认证方法
//将默认的认证方法替换成我们自己定义的认证方法
auth.userDetailsService(userDetailsService)
//设置密码的加密方式,需要使用springSecurity自带的
.passwordEncoder(passwordEncoder);
}
/**
* 一般是实现一些请求的过滤和拦截
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//设置拦截忽略文件夹,可以对静态资源放行
web.ignoring().antMatchers("/css/**", "/js/**");
}
/**
* 请求
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//第一步:解决跨域问题,cors预检请求放行,让spring security 放行所有预请求(preflight request)(cors 预检请求)
http.authorizeRequests().requestMatchers(CorsUtils::isPreFlightRequest).permitAll();
//第二步:让spring security永远不会创建HttpSession,它不会使用HttpSession来获取
//disable().sessionManagement() 禁用session管理器 cacheControl缓存器
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().headers().cacheControl();
//第三步:在UsernamePasswordAuthenticationFilter登录认证 之前添加 JwtAuthenticationTokenFilter 进行token认证
http.addFilterBefore(oncePerRequestFilter, UsernamePasswordAuthenticationFilter.class);
//csrf()为黑客利用用户的登录状态进行一些操作
//所有处理来自浏览器的请求需要是CSRF保护,如果后台服务是提供API调用那么可能就要禁用CSRF保护
//springsecurity是默认开启csrf保护的
//http.cors().and().csrf().disable();
//定接口"/getUser"只能是拥有"query_user"权限的用户可以访问
//authorize授权; 批准
//这句话的意思是 如果想要访问/getUser这个路径,必须拥有query_user这个权限
//此时访问这个getUser这个路径 会报403Forbidden代表权限不足的错误 正确 因为我们没有该权限
//401错误代表没有登录
//http.authorizeRequests();
//限制同一账号只能一个用户使用(无效果)
http.sessionManagement().maximumSessions(1)
//用户被挤下线
.expiredSessionStrategy(sessionAuthenticationStrategy);
http.formLogin().permitAll()
//登录成功时替换为我们自定义登录成功的处理器
.successHandler(authenticationSuccessHandler)
//登录失败时替换为我们自定义登录失败的处理器
.failureHandler(authenticationFailureHandler);
http.logout().permitAll()
//注销成功的处理器
.logoutSuccessHandler(logoutSuccessHandler)
//登出之后删除cookie;
//jsessionid是一个Cookie,可以通过在URL后面加上“;jsessionid=xxx”来传递“session id”;
//其中Servlet容器用来记录用户session,当我们创建回话时会自动创建,用来记录用户的访问记录
.deleteCookies("JSESSIONID");
http.authorizeRequests().
withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
o.setAccessDecisionManager(accessDecisionManager);//访问决策管理器
o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource);//安全元数据源
return o;
}
});
http.addFilterBefore((Filter) abstractSecurityInterceptor, FilterSecurityInterceptor.class);//增加到默认拦截链中
//异常处理(权限拒绝、登录失效等)
//匿名用户访问无权限资源时的异常处理
//账号被挤下线就是用户已经登录
http.exceptionHandling().
authenticationEntryPoint(authenticationEntryPoint);
}
}