spring security
项目源码:https://gitee.com/shiruixian/spring-security-project
(一)介绍
一、简介
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。提供了完善的认证机制和方法级的授权功能。是一款非常优秀的权限管理框架。
spring security 的核心功能主要包括:
- 认证 (你是谁)
- 授权 (你能干什么)
- 攻击防护 (防止伪造身份)
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份。
二、过滤器链
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的各个进行说明:
-
WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于处理异步请求映射的 WebAsyncManager 进行集成。
-
SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信息就是这个过滤器处理的。
-
HeaderWriterFilter:用于将头信息加入响应中。
-
CsrfFilter:用于处理跨站请求伪造。
-
LogoutFilter:用于处理退出登录。
-
UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
-
DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
-
BasicAuthenticationFilter:检测和处理 http basic 认证。
-
RequestCacheAwareFilter:用来处理请求的缓存。
-
SecurityContextHolderAwareRequestFilter:主要是包装请求对象request。
-
AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
-
SessionManagementFilter:管理 session 的过滤器
-
ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。
-
FilterSecurityInterceptor:可以看做过滤器链的出口。
-
RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember-me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
三、流程
流程说明:
- 客户端发起一个请求,进入 Security 过滤器链。
- 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
- 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到 AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
- 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则到 Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
四、配置类
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证
auth.userDetailsService(userDetailsService());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//http相关的配置,包括登入登出、异常处理、会话管理等
http.cors();//开启跨域
http.csrf().disable();//关闭csrf
http.authorizeRequests()
// .antMatchers("/user/getUser").hasAuthority("query_user")
// .antMatchers("/user/getAllUser").permitAll()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
//决策管理器
o.setAccessDecisionManager(accessDecisionManager);
//安全元数据源
o.setSecurityMetadataSource(securityMetadataSource);
return o;
}
})
//登出允许所有用户
.and().logout().permitAll()
//登出成功处理逻辑
.logoutSuccessHandler(logoutSuccessHandler)
//登出之后删除cookie
.deleteCookies("JSESSIONID")
//使HttpSession失效
.invalidateHttpSession(true)
//登入允许所有用户
.and().formLogin().permitAll()
//登录成功处理逻辑
.successHandler(authenticationSuccessHandler)
//登录失败处理逻辑
.failureHandler(authenticationFailureHandler)
//记住我
.and().rememberMe()
.rememberMeParameter("remember_me")
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60 * 60 * 24 * 30)
.userDetailsService(userDetailsService())
//异常处理(权限拒绝、登录失效等)
.and().exceptionHandling()
//匿名用户访问无权限资源时的异常处理;
.authenticationEntryPoint(authenticationEntryPoint)
//权限拒绝处理逻辑
.accessDeniedHandler(accessDeniedHandler)
//匿名用户访问无权限资源时的异常处理
//.authenticationEntryPoint(authenticationEntryPoint)
//会话管理
//同一账号同时登录最大用户数
.and()
.sessionManagement()
.maximumSessions(1)
//是否保留已经登录的用户;为true,新用户无法登录;为 false,旧用户被踢出
.maxSessionsPreventsLogin(false)
//会话失效(账号被挤下线)处理逻辑
.expiredSessionStrategy(sessionInformationExpiredStrategy)
//踢出用户配置
.sessionRegistry(sessionRegistry());
//添加权限认证过滤器
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
//添加过滤器,放行options等
http.addFilterAfter(optionsFilter(), ChannelProcessingFilter.class);
//添加vaptcha登录验证过滤器
http.addFilterBefore(vaptchaFilterr(), UsernamePasswordAuthenticationFilter.class);
}
//添加踢出用户配置
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
//vaptcha登录验证过滤器
@Bean
public VaptchaFilterr vaptchaFilterr(){
return new VaptchaFilterr();
}
//实现记住我
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//在第一次运行时会创建一个记住我token数据库,只能运行一次
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
//放行options,跨域等配置
@Bean
OptionsFilter optionsFilter() {
return new OptionsFilter();
}
@Override
@Bean
public UserDetailsService userDetailsService() {
//获取用户账号密码及权限信息
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式(强hash方式加密)
return new BCryptPasswordEncoder();
}
}
说明:
- configure(AuthenticationManagerBuilder auth)
AuthenticationManager 的建造器,配置 AuthenticationManagerBuilder 会让Security 自动构建一个 AuthenticationManager(该类的功能参考流程图);如果想要使用该功能你需要配置一个 UserDetailService 和 PasswordEncoder。UserDetailsService 用于在认证器中根据用户传过来的用户名查找一个用户, PasswordEncoder 用于密码的加密与比对,我们存储用户密码的时候用PasswordEncoder.encode() 加密存储,在认证器里会调用 PasswordEncoder.matches() 方法进行密码比对。如果重写了该方法,Security 会启用 DaoAuthenticationProvider 这个认证器,该认证就是先调用 UserDetailsService.loadUserByUsername 然后使用 PasswordEncoder.matches() 进行密码比对,如果认证成功成功则返回一个 Authentication 对象。
- configure(HttpSecurity http)
配置登录、登出、鉴权和权限等。
(二)示例
一、准备工作
1、统一错误码枚举
public enum ResultCode {
/* 成功 */
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, "账号下线"),
/* 业务错误 */
NO_PERMISSION(3001, "没有权限"),
/* vaptcha验证*/
VAPTCHA_CHECK_FAIL(4001, "验证失败"),
VAPTCHA_TOKEN_NULL(4002, "token值为空"),
VAPTCHA_SERVER_NULL(4003, "server值为空");
/**
* 返回码
*/
private Integer code;
/**
* 返回信息
*/
private String message;
ResultCode(Integer code, String message) {
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
/**
* 根据code获取message
*
* @param code
* @return
*/
public static String getMessageByCode(Integer code) {
for (ResultCode ele : values()) {
if (ele.getCode().equals(code)) {
return ele.getMessage();
}
}
return null;
}
}
2、统一JSON返回体
public class JsonResult<T> implements Serializable {
private Boolean success;
private Integer errorCode;
private String errorMsg;
private T data;
public JsonResult() {
}
public JsonResult(boolean success) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
}
public JsonResult(boolean success, ResultCode resultEnum) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
}
public JsonResult(boolean success, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
this.data = data;
}
public JsonResult(boolean success, ResultCode resultEnum, T data) {
this.success = success;
this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
this.data = data;
}
public Boolean getSuccess() {
return success;
}
public void setSuccess(Boolean success) {
this.success = success;
}
public Integer getErrorCode() {
return errorCode;
}
public void setErrorCode(Integer errorCode) {
this.errorCode = errorCode;
}
public String getErrorMsg() {
return errorMsg;
}
public void setErrorMsg(String errorMsg) {
this.errorMsg = errorMsg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
3、返回体构造工具
public class ResultTool {
public static JsonResult success() {
return new JsonResult(true);
}
public static <T> JsonResult<T> success(T data) {
return new JsonResult(true, data);
}
public static JsonResult fail() {
return new JsonResult(false);
}
public static JsonResult fail(ResultCode resultEnum) {
return new JsonResult(false, resultEnum);
}
}
4、pom文件
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
<scope>provided</scope>
</dependency>
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- Hikari连接池-->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<exclusions>
<!-- 排除 tomcat-jdbc 以使用 HikariCP -->
<exclusion>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--JSON-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4.1</version>
</dependency>
5、配置文件
# 应用名称
spring:
application:
name: security-project
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai
username: root
password: root
hikari:
minimum-idle: 5
idle-timeout: 600000
maximum-pool-size: 10
auto-commit: true
pool-name: MyHikariCP
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
# 应用服务 WEB 访问端口
server:
port: 8888
# mybatis-plus
mybatis-plus:
# 如果是放在resource目录 classpath:/mapper/*Mapper.xml
mapper-locations: classpath:/mapper/*Mapper.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.lunz.securityproject.entity
global-config:
#主键类型 0:"数据库ID自增", 1:"用户输入ID",2:"全局唯一ID (数字类型唯一ID)", 3:"全局唯一ID UUID";
id-type: 0
#字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
field-strategy: 1
#驼峰下划线转换
db-column-underline: true
#刷新mapper 调试神器
refresh-mapper: true
#数据库大写下划线转换
#capital-mode: true
#序列接口实现类配置,不在推荐使用此方式进行配置,请使用自定义bean注入
#key-generator: com.baomidou.mybatisplus.incrementer.H2KeyGenerator
#逻辑删除配置(下面3个配置)
logic-delete-value: 0
logic-not-delete-value: 1
#自定义sql注入器,不在推荐使用此方式进行配置,请使用自定义bean注入
#sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
#自定义填充策略接口实现,不在推荐使用此方式进行配置,请使用自定义bean注入
# meta-object-handler: com.baomidou.springboot.MyMetaObjectHandler
#自定义SQL注入器
#sql-injector: com.baomidou.springboot.xxx
# SQL 解析缓存,开启后多租户 @SqlParser 注解生效
sql-parser-cache: true
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
二、数据库表设计
1.用户表
sys_user
字段名 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
account | varchar | 账号 |
user_name | varchar | 用户名 |
password | varchar | 密码 |
last_login_time | datetime | 上一次登录时间 |
enabled | tinyint | 是否可用(默认1可用) |
account_not_expired | tinyint | 是否过期(默认1未过期) |
account_not_locked | tinyint | 是否锁定(默认1未锁定) |
credentials_not_expired | tinyint | 证书(密码)是否过期(默认1未过期) |
create_time | datetime | 创建时间 |
update_time | datetime | 修改时间 |
create table sys_user
(
id int auto_increment
primary key,
account varchar(32) not null comment '账号',
user_name varchar(32) not null comment '用户名',
password varchar(64) null comment '用户密码',
last_login_time datetime null comment '上一次登录时间',
enabled tinyint(1) default 1 null comment '账号是否可用。默认为1(可用)',
account_not_expired tinyint(1) default 1 null comment '是否过期。默认为1(没有过期)',
account_not_locked tinyint(1) default 1 null comment '账号是否锁定。默认为1(没有锁定)',
credentials_not_expired tinyint(1) default 1 null comment '证书(密码)是否过期。默认为1(没有过期)',
create_time datetime null comment '创建时间',
update_time datetime null comment '修改时间'
)
comment '用户表';
INSERT INTO sys_user (id, account, user_name, password, last_login_time, enabled, account_not_expired, account_not_locked, credentials_not_expired, create_time, update_time) VALUES (1, 'zhangsan', '张三', '$2a$10$47lsFAUlWixWG17Ca3M/r.EPJVIb7Tv26ZaxhzqN65nXVcAhHQM4i', '2019-09-04 20:25:36', 1, 1, 1, 1, '2019-08-29 06:28:36', '2019-09-04 20:25:36');
INSERT INTO sys_user (id, account, user_name, password, last_login_time, enabled, account_not_expired, account_not_locked, credentials_not_expired, create_time, update_time) VALUES (2, 'lisi', '李四', '$2a$10$uSLAeON6HWrPbPCtyqPRj.hvZfeM.tiVDZm24/gRqm4opVze1cVvC', '2019-09-05 00:07:12', 1, 1, 1, 1, '2019-08-29 06:29:24', '2019-09-05 00:07:12');
注:上方用户表中插入的密码明文都是123456。
2.角色表
sys_role
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
role_code | varchar | 角色代码 |
role_name | varchar | 角色名称 |
role_description | varchar | 角色描述 |
create table sys_role
(
id int auto_increment comment '主键id' primary key,
role_code varchar(32) null comment '角色代码',
role_name varchar(32) null comment '角色名',
role_description varchar(64) null comment '角色说明'
)
comment '用户角色表';
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (1, 'admin', '管理员', '管理员,拥有所有权限');
INSERT INTO sys_role (id, role_code, role_name, role_description) VALUES (2, 'user', '普通用户', '普通用户,拥有部分权限');
3.权限表
sys_permission
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
permission_code | varchar | 权限code |
permission_name | varchar | 权限名 |
create table sys_permission
(
id int auto_increment comment '主键id' primary key,
permission_code varchar(32) null comment '权限code',
permission_name varchar(32) null comment '权限名'
)
comment '权限表';
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (1, 'create_user', '创建用户');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (2, 'query_user', '查看用户');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (3, 'delete_user', '删除用户');
INSERT INTO sys_permission (id, permission_code, permission_name) VALUES (4, 'modify_user', '修改用户');
4、用户角色关联表
sys_user_role_relation
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
user_id | int | 用户id |
role_id | int | 角色id |
create table sys_user_role_relation
(
id int auto_increment comment '主键id' primary key,
user_id int null comment '用户id',
role_id int null comment '角色id'
)
comment '用户角色关联关系表';
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (1, 1, 1);
INSERT INTO sys_user_role_relation (id, user_id, role_id) VALUES (2, 2, 2);
5.角色权限关联表
sys_role_permission_relation
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
role_id | int | 角色id |
permission_id | int | 权限id |
create table sys_role_permission_relation
(
id int auto_increment comment '主键id' primary key,
role_id int null comment '角色id',
permission_id int null comment '权限id'
)
comment '角色-权限关联关系表';
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (1, 1, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (2, 1, 2);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (3, 1, 3);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (4, 1, 4);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (5, 2, 1);
INSERT INTO sys_role_permission_relation (id, role_id, permission_id) VALUES (6, 2, 2);
6.请求路径表
sys_request_path
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
url | varchar | 请求路径 |
description | varchar | 路径描述 |
create table sys_request_path
(
id int auto_increment comment '主键id' primary key,
url varchar(64) not null comment '请求路径',
description varchar(128) null comment '路径描述'
)
comment '请求路径';
INSERT INTO sys_request_path (id, url, description) VALUES (1, '/user/getUser', '查询用户');
7.路径权限关联表
sys_request_path_permission_relation
字段 | 类型 | 描述 |
---|---|---|
id | int | 主键 |
url_id | int | 请求路径id |
permission_id | int | 权限id |
create table sys_request_path_permission_relation
(
id int null comment '主键id',
url_id int null comment '请求路径id',
permission_id int null comment '权限id'
)
comment '路径权限关联表';
INSERT INTO sys_request_path_permission_relation (id, url_id, permission_id) VALUES (null, 1, 2);
8.token表
persistent_logins
字段 | 类型 | 描述 |
---|---|---|
username | varchar | 账号 |
series | varchar | 标识 |
token | varchar | token |
last_used | timestamp | 最后使用 |
create table persistent_logins
(
username varchar(64) null comment '账号',
series varchar(64) primary key comment '标识',
token varchar(64) null comment 'token',
last_used timestamp null
)
comment 'token表';
三、核心配置文件
创建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相关的配置,包括登入登出、异常处理、会话管理等
http.cors();//开启跨域
http.csrf().disable();//关闭csrf
}
}
四、配置跨域
虽然在WebSecurityConfig
中配置了跨域:http.cors()
,后端也可正常返回数据,但有些浏览器仍会抛出跨域异常,所以需要再次配置跨域。
1、编写过滤器:
public class OptionsFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
httpServletResponse.setHeader("Access-Control-Allow-Origin",httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, token, Accept,appkey");
httpServletResponse.setHeader("Access-Control-Expose-Headers", "*");
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
2、加入配置
//添加过滤器,放行options等
//ChannelProcessingFilter为决定 web请求的通道,即 http或https
http.addFilterAfter(optionsFilter(), ChannelProcessingFilter.class);
-----------------------------------------------------------------------
/**
* 配置过滤器,放行options等
* @return
*/
@Bean
public OptionsFilter optionsFilter(){
return new OptionsFilter();
}
五、用户登录认证逻辑
1、创建自定义UserDetailsService
这是实现自定义用户认证的核心逻辑,loadUserByUsername(String username)的参数就是登录时提交的用户名,返回类型是一个叫UserDetails 的接口,需要在这里构造出他的一个实现类User,这是Spring security提供的用户信息实体。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//需要构造出 org.springframework.security.core.userdetails.User 对象并返回
return null;
}
}
2、准备service层和mapper层方法
1.根据用户名查询用户信息
1.实体类
@Data
@TableName("sys_user")
public class SysUser implements Serializable {
private static final long serialVersionUID = 915478504870211231L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
//账号
@TableField(value = "account")
private String account;
//用户名
@TableField(value = "user_name")
private String userName;
//用户密码
@TableField(value = "password")
private String password;
//上一次登录时间
@TableField(value = "last_login_time")
private Date lastLoginTime;
//账号是否可用。默认为1(可用)
@TableField(value = "enabled")
private Boolean enabled;
//是否过期。默认为1(没有过期)
@TableField(value = "account_not_expired")
private Boolean accountNonExpired;
//账号是否锁定。默认为1(没有锁定)
@TableField(value = "account_not_locked")
private Boolean accountNonLocked;
//证书(密码)是否过期。默认为1(没有过期)
@TableField(value = "credentials_not_expired")
private Boolean credentialsNonExpired;
//创建时间
@TableField(value = "create_time")
private Date createTime;
//修改时间
@TableField(value = "update_time")
private Date updateTime;
}
2.mapper
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
/**
* 通过账号获取用户
* @param userName 用户账号
* @return
*/
SysUser selectByName(String userName);
}
3.xml
<mapper namespace="com.lunz.securityproject.mapper.SysUserMapper">
<resultMap type="com.lunz.securityproject.entity.domain.SysUser" id="SysUserMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="account" column="account" jdbcType="VARCHAR"/>
<result property="userName" column="user_name" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<result property="lastLoginTime" column="last_login_time" jdbcType="TIMESTAMP"/>
<result property="enabled" column="enabled" jdbcType="BOOLEAN"/>
<result property="accountNonExpired" column="account_not_expired" jdbcType="BOOLEAN"/>
<result property="accountNonLocked" column="account_not_locked" jdbcType="BOOLEAN"/>
<result property="credentialsNonExpired" column="credentials_not_expired" jdbcType="BOOLEAN"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
<result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/>
<result property="createUser" column="create_user" jdbcType="INTEGER"/>
<result property="updateUser" column="update_user" jdbcType="INTEGER"/>
</resultMap>
<select id="selectByName" parameterType="String" resultMap="SysUserMap">
select * from sys_user where account = #{userName}
</select>
</mapper>
4.service
public interface SysUserService extends IService<SysUser> {
/**
* 通过账号查找
* @param userName 用户账号
* @return
*/
SysUser selectByName(String userName);
}
5.impl
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Override
public SysUser selectByName(String userName) {
return baseMapper.selectByName(userName);
}
}
2.根据用户名查询用户的权限信息
1.实体类
@Data
@TableName("sys_permission")
public class SysPermission implements Serializable {
private static final long serialVersionUID = -71969734644822184L;
//主键id
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
//权限code
@TableField(value = "permission_code")
private String permissionCode;
//权限名
@TableField(value = "permission_name")
private String permissionName;
}
2.mapper
@Repository
public interface SysPermissionMapper {
/**
* 根据用户名查询用户的权限信息
* @param userId
* @return
*/
List<SysPermission> selectListByUser(Integer userId);
}
3.xml
<mapper namespace="com.lunz.securityproject.mapper.SysPermissionMapper">
<resultMap type="com.lunz.securityproject.entity.domain.SysPermission" id="SysPermissionMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="permissionCode" column="permission_code" jdbcType="VARCHAR"/>
<result property="permissionName" column="permission_name" jdbcType="VARCHAR"/>
</resultMap>
<select id="selectListByUser" resultMap="SysPermissionMap">
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>
</mapper>
4.service
public interface SysPermissionService {
/**
* 查询用户的权限列表
* @param userId
* @return
*/
List<SysPermission> selectListByUser(Integer userId);
}
5.impl
@Service
public class SysPermissionServiceImpl implements SysPermissionService {
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Override
public List<SysPermission> selectListByUser(Integer userId) {
return sysPermissionMapper.selectListByUser(userId);
}
}
这样的话流程我们就理清楚了,首先根据用户名查出对应用户,再拿得到的用户的用户id去查询它所拥有的的权限列表,最后构造出我们需要的org.springframework.security.core.userdetails.User对象。
接下来改造一下刚刚自定义的UserDetailsService。
3.重写loadUserByUsername
方法
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Autowired
private SysPermissionService sysPermissionService;
/**
* @param username
* @return
* 这是实现自定义用户认证的核心逻辑,loadUserByUsername(String username)的参数就是登录时提交的用户名,返回类型是一个叫UserDetails 的接口,
* 需要在这里构造出他的一个实现类User,这是Spring security提供的用户信息实体。
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username == null || "".equals(username)){
throw new UsernameNotFoundException("用户名为空");
}
//根据用户名查询用户
SysUser user = sysUserService.selectByName(username);
if(user == null){
throw new RuntimeException("用户不存在");
}
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//获取该用户所拥有的权限
List<SysPermission> sysPermissions = sysPermissionService.selectListByUser(user.getId());
// 声明用户授权
sysPermissions.forEach(sysPermission -> {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(sysPermission.getPermissionCode());
grantedAuthorities.add(grantedAuthority);
});
return new User(user.getAccount(), user.getPassword(), user.getEnabled(), user.getAccountNonExpired(),
user.getCredentialsNonExpired(), user.getAccountNonLocked(), grantedAuthorities);
}
}
3、加入配置
将我们的自定义的基于JDBC的用户认证在之前创建的WebSecurityConfig 中得configure(AuthenticationManagerBuilder auth)中声明一下,到此自定义的基于JDBC的用户认证就完成了。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//配置认证方式等
auth.userDetailsService(userDetailsService());
}
/**
* 获取用户账号密码及权限信息
* @return
*/
@Bean
@Override
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
六、用户密码加密
新版本的Spring security规定必须设置一个默认的加密方式,不允许使用明文。这个加密方式是用于在登录时验证密码、注册时需要用到。
我们可以自己选择一种加密方式,Spring security为我们提供了多种加密方式,我们这里使用一种强hash方式进行加密。
在WebSecurityConfig 中注入(注入即可,不用声明使用),这样就会对提交的密码进行加密处理了,如果你没有注入加密方式,运行的时候会报错"There is no PasswordEncoder mapped for the id"错误。
/**
* 设置密码加密方式
* @return
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 设置默认的加密方式(强hash方式加密)
return new BCryptPasswordEncoder();
}
同样的我们数据库里存储的密码也要用同样的加密方式存储,例如我们将123456用BCryptPasswordEncoder 加密后存储到数据库中(注意:即使是同一个明文用这种加密方式加密出来的密文也是不同的,这就是这种加密方式的特点)
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
bCryptPasswordEncoder.encode(sysUser.getPassword());
七、屏蔽默认登录重定向
1、进行接口测试
创建controller层:
@RequestMapping("user")
@RestController
@CrossOrigin
public class UserController {
@Autowired
private SysUserService sysUserService;
@GetMapping("/getUser")
public JsonResult getUser(){
SysUser user = sysUserService.selectByName("user2");
return ResultTool.success(user);
}
}
启动项目后用postman访问接口会返回401,则代表该请求被拒绝了。
在前后端分离的情况下(比如前台使用VUE或JQ等)我们需要的是在前台接收到"用户未登录"的提示信息,所以我们接下来要做的就是屏蔽401提示,返回json格式的返回体。而实现这一功能的核心就是实现AuthenticationEntryPoint
并在WebSecurityConfig
中注入,然后在configure(HttpSecurity http)
方法中配置。AuthenticationEntryPoint
主要是用来处理匿名用户访问无权限资源时的异常(即未登录,或者登录状态过期失效)。
CustomizeAuthenticationEntryPoint类:
@Component
public class CustomizeAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
配置:
//匿名用户访问无权限资源时的异常4
@Autowired
CustomizeAuthenticationEntryPoint authenticationEntryPoint;
-----------------------------------------------------
//异常处理(权限拒绝、登录失效等)
.and().exceptionHandling()
//匿名用户访问无权限资源时的异常处理;
.authenticationEntryPoint(authenticationEntryPoint);
此时再次请求该接口则返回指定的JSON数据。
2、实现登录成功/失败,登出逻辑
对于登入登出我们都不需要自己编写controller接口,Spring Security为我们封装好了。默认登入路径:/login
,登出路径:/logout
。
当登录成功或登录失败都需要返回统一的json返回体给前台,前台才能知道对应的做什么处理。
而实现登录成功和失败的异常处理需要分别实现AuthenticationSuccessHandler
和AuthenticationFailureHandler
接口并在WebSecurityConfig
中注入,然后在configure(HttpSecurity http)
方法中然后声明。
1.登录成功
CustomizeAuthenticationSuccessHandler类:
@Component
public class CustomizeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private SysUserService sysUserService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//更新用户表上次登录时间、更新人、更新时间等字段
User userDetails = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
SysUser sysUser = sysUserService.selectByName(userDetails.getUsername());
sysUser.setLastLoginTime(new Date());
sysUser.setUpdateTime(new Date());
sysUserService.update(sysUser);
//此处还可以进行一些处理,比如登录成功之后可能需要返回给前台当前用户有哪些菜单权限,
//进而前台动态的控制菜单的显示等,具体根据自己的业务需求进行扩展
//返回json数据
JsonResult result = ResultTool.success();
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(result));
}
}
SysUserService:
/**
* 更新用户信息并返回新的用户信息
* @param sysUser
* @return
*/
SysUser update(SysUser sysUser);
SysUserServiceImpl:
@Override
public SysUser update(SysUser sysUser) {
baseMapper.updateById(sysUser);
return baseMapper.selectById(sysUser.getId());
}
2.登录失败
登录失败处理器主要用来对登录失败的场景(密码错误、账号锁定等…)做统一处理并返回给前台统一的json返回体。
CustomizeAuthenticationFailureHandler 类:
@Component
public class CustomizeAuthenticationFailureHandler implements AuthenticationFailureHandler{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//返回json数据
JsonResult result = null;
if (exception instanceof AccountExpiredException) {
//账号过期
result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
} else if (exception instanceof BadCredentialsException) {
//密码错误
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
} else if (exception instanceof CredentialsExpiredException) {
//密码过期
result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
} else if (exception instanceof DisabledException) {
//账号不可用
result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
} else if (exception instanceof LockedException) {
//账号锁定
result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
} else if (exception instanceof InternalAuthenticationServiceException) {
//用户不存在
result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
}else{
//其他错误
result = ResultTool.fail(ResultCode.COMMON_FAIL);
}
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
response.getWriter().write(JSON.toJSONString(result));
}
}
3.登出
同样的登出也要将登出成功时结果返回给前台,并且登出之后进行将cookie失效或删除。
CustomizeLogoutSuccessHandler类:
@Component
public class CustomizeLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
JsonResult result = ResultTool.success();
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
4.加入配置
//登录成功处理逻辑1
@Autowired
CustomizeAuthenticationSuccessHandler authenticationSuccessHandler;
//登录失败处理逻辑2
@Autowired
CustomizeAuthenticationFailureHandler authenticationFailureHandler;
//登出成功处理逻辑6
@Autowired
CustomizeLogoutSuccessHandler logoutSuccessHandler;
------------------------------------------------------------------
.and().logout().permitAll()
//登出成功处理逻辑
.logoutSuccessHandler(logoutSuccessHandler)
//登出之后删除cookie
.deleteCookies("JSESSIONID")
//使HttpSession失效
.invalidateHttpSession(true)
//登入允许所有用户
.and().formLogin().permitAll()
//登录成功处理逻辑
.successHandler(authenticationSuccessHandler)
//登录失败处理逻辑
.failureHandler(authenticationFailureHandler)
注意:登录成功之后后端需要cookie,可在vue的main.js中添加axios.defaults.withCredentials=true;
使请求自动带上cookie。
八、会话管理
1、限制账号登录数量
例如限制同一账号最大登录数
//启动会话管理
.and().sessionManagement()
//设置最大登录数为1
.maximumSessions(1)
//当登录数达最大值时,是否保留已登录用户(true:保留,新用户无法登录;false:不保留,踢出旧用户)
.maxSessionsPreventsLogin(false);
2、账号被挤下线处理逻辑
当账号异地登录导致被挤下线时也要返回给前端json格式的数据,比如提示"账号下线"、"您的账号在异地登录,是否是您自己操作"或者"您的账号在异地登录,可能由于密码泄露,建议修改密码"等。这时就要实现SessionInformationExpiredStrategy(会话信息过期策略)来自定义会话过期时的处理逻辑。
@Component
public class CustomizeSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.USER_ACCOUNT_USE_BY_OTHERS);
HttpServletResponse httpServletResponse = event.getResponse();
httpServletResponse.setContentType("text/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(result));
}
}
注入并声明配置:
//会话失效(账号被挤下线)处理逻辑5
@Autowired
CustomizeSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
------------------------------------------------------------------------------------
//启动会话管理
.and().sessionManagement()
//设置最大登录数为1
.maximumSessions(1)
//当登录数达最大值时,是否保留已登录用户(true:保留,新用户无法登录;false:不保留,踢出旧用户)
.maxSessionsPreventsLogin(false)
//处理账号失效、被挤下线
.expiredSessionStrategy(sessionInformationExpiredStrategy);
此时在浏览器A中登录账号,请求接口,可正常请求;再向B浏览器登录该账号,也可正常访问接口,但再返回浏览器A中请求接口则返回账号下线提醒。
九、基于JDBC的动态权限控制
关于接口权限的配置可以使用http.authorizeRequests().antMatchers("/user/getUser").hasAuthority("query_user");
来配置,但在正常开发中这么写会很麻烦,所以需要将权限控制的资源配到数据库中,当然也可以存储在其他地方,比如用一个枚举。
我们需要实现一个AccessDecisionManager(访问决策管理器),在里面我们对当前请求的资源进行权限判断,判断当前登录用户是否拥有该权限,如果有就放行,如果没有就抛出一个"权限不足"的异常。不过在实现AccessDecisionManager之前我们还需要做一件事,那就是拦截到当前的请求,并根据请求路径从数据库中查出当前资源路径需要哪些权限才能访问,然后将查出的需要的权限列表交给AccessDecisionManager去处理后续逻辑。那就是需要先实现一个SecurityMetadataSource(安全元数据源),我们这里使用他的一个子类FilterInvocationSecurityMetadataSource。
在自定义的SecurityMetadataSource编写好之后,我们还要编写一个CustomizeAbstractSecurityInterceptor(权限拦截器),增加到Spring security默认的拦截器链中,以达到拦截的目的。
同样的最后需要在WebSecurityConfig中注入,并在configure(HttpSecurity http)方法中声明。
1、权限拦截器
@Component
public class CustomizeAbstractSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(CustomizeAccessDecisionManager accessDecisionManager) {
super.setAccessDecisionManager(accessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
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);
}
}
}
2、安全元数据源
@Component
public class CustomizeFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Autowired
SysPermissionService sysPermissionService;
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
//获取请求地址
String requestUrl = ((FilterInvocation) o).getRequestUrl();
//查询具体某个接口的权限
List<SysPermission> permissionList = sysPermissionService.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;
}
}
3、访问决策管理器
@Component
public class CustomizeAccessDecisionManager 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;
}
}
4、查询请求路径权限
mapper:
/**
* 查询具体某个接口的权限
*
* @param path 接口路径
* @return
*/
List<SysPermission> selectListByPath(String path);
xml:
<select id="selectListByPath" resultMap="SysPermissionMap">
SELECT
pe.*
FROM
sys_permission pe
LEFT JOIN sys_request_path_permission_relation re ON re.permission_id = pe.id
LEFT JOIN sys_request_path pa ON pa.id = re.url_id
WHERE pa.url = #{path}
</select>
service:
/**
* 查询具体某个接口的权限
*
* @param path
* @return
*/
List<SysPermission> selectListByPath(String path);
impl:
@Override
public List<SysPermission> selectListByPath(String path) {
return sysPermissionMapper.selectListByPath(path);
}
5、权限拒绝处理
@Component
public class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
JsonResult result = ResultTool.fail(ResultCode.NO_PERMISSION);
response.setContentType("text/json;charset=utf-8");
response.getWriter().write(JSON.toJSONString(result));
}
}
6、加入配置
//权限拒绝处理逻辑3
@Autowired
CustomizeAccessDeniedHandler accessDeniedHandler;
//访问决策管理器7
@Autowired
CustomizeAccessDecisionManager accessDecisionManager;
//实现权限拦截8
@Autowired
CustomizeFilterInvocationSecurityMetadataSource securityMetadataSource;
//权限拦截器9
@Autowired
private CustomizeAbstractSecurityInterceptor securityInterceptor;
---------------------------------------------------------------------------
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
//决策管理器
o.setAccessDecisionManager(accessDecisionManager);
//安全元数据源
o.setSecurityMetadataSource(securityMetadataSource);
return o;
}
})
--------------------------------------------------------------------------
在异常处理后面加入权限不足处理逻辑
//异常处理(权限拒绝、登录失效等)
.and().exceptionHandling()
//匿名用户访问无权限资源时的异常处理;
.authenticationEntryPoint(authenticationEntryPoint)
//权限不足处理逻辑
.accessDeniedHandler(accessDeniedHandler)
--------------------------------------------------------------------------
//将权限拦截器增加到默认拦截链中
http.addFilterBefore(securityInterceptor, FilterSecurityInterceptor.class);
十、其他功能
1、记住我
//导入数据源,实现记住我
@Autowired
private DataSource dataSource;
----------------------------------------------------
//记住我
.and().rememberMe()
//设置前端传过来的记住我的名称
.rememberMeParameter("remember_me")
//使用token仓库
.tokenRepository(persistentTokenRepository())
//设置token存在时间,单位为秒
.tokenValiditySeconds(60 * 60 * 24 * 30)
.userDetailsService(userDetailsService())
-----------------------------------------------------
/**
* 实现记住我
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
//在第一次运行时会创建一个记住我token数据库,只能运行一次,上面已经把表建好了,所以该语句不需要执行
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
2、登录验证
本次使用vaptcha进行验证,该验证同样可以使用到注册、修改密码、注销账号等操作。
1.创建实体类
@Getter
@Setter
@ToString
public class Vaptcha {
private String id;
private String secretkey;
private Integer scene;
private String token;
private String ip;
private String userid;
public Vaptcha() {
// TODO Auto-generated constructor stub
}
public Vaptcha(String id, String secretkey, Integer scene, String token, String ip, String userid) {
super();
this.id = id;
this.secretkey = secretkey;
this.scene = scene;
this.token = token;
this.ip = ip;
this.userid = userid;
}
}
@Getter
@Setter
@ToString
public class VaptchaResult {
private String msg;
private Integer score;
private Integer success;
public VaptchaResult() {
// TODO Auto-generated constructor stub
}
public VaptchaResult(String msg, Integer score, Integer success) {
super();
this.msg = msg;
this.score = score;
this.success = success;
}
}
2.创建工具类
@Component
public class VaptchaUtil {
public static Integer postJSON(String url, String token) throws Exception{
Vaptcha v = new Vaptcha();
v.setId("61137f3cc108cd3a30dddf5c");
v.setSecretkey("dc58db9aec9f4df5b728d06bdae18112");
v.setScene(1);
v.setIp("127.0.0.1");
v.setToken(token);
String json = JSON.toJSONString(v);
StringEntity entity = new StringEntity(json, Charsets.UTF_8);
String j = postRequest(url, entity);
VaptchaResult vr = JSON.parseObject(j,VaptchaResult.class);
return vr.getSuccess();
}
// 发送POST请求
private static String postRequest(String url, HttpEntity entity) throws Exception {
HttpPost post = new HttpPost(url);
post.addHeader("Content-Type", "application/json");
post.addHeader("Accept", "application/json");
post.setEntity(entity);
try {
CloseableHttpClient client = HttpClientBuilder.create().build();
HttpResponse response = client.execute(post);
int code = response.getStatusLine().getStatusCode();
if (code >= 400) {
throw new Exception(EntityUtils.toString(response.getEntity()));
}
return EntityUtils.toString(response.getEntity());
} catch (ClientProtocolException e) {
throw new Exception("postRequest -- Client protocol exception!", e);
} catch (IOException e) {
throw new Exception("postRequest -- IO error!", e);
} finally {
post.releaseConnection();
}
}
}
3.创建vaptcha过滤器
public class VaptchaFilterr extends OncePerRequestFilter {
/**
* 需要vaptcha验证的接口
*/
private String[] urls = new String[]{"/login", "/user/register", "/user/updatePassword" ,"/user/deleteUser"};
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//判断请求接口是否是需要验证的接口
boolean b = false;
String url = request.getRequestURI();
for (String u : urls) {
if(u.equals(url)){
b = true;
}
}
if(b){
//判断是否为post,不允许非post请求
if(!"POST".equals(request.getMethod())){
throw new AuthenticationServiceException("不支持该请求类型: " + request.getMethod());
}
String token = request.getParameter("token");
String server = request.getParameter("server");
//token与server值为空
if(token == null || token.length() <= 0 || "".equals(token)){
failResult(response, ResultCode.VAPTCHA_TOKEN_NULL);
return;
}
if(server == null || server.length() <= 0 || "".equals(server)){
failResult(response, ResultCode.VAPTCHA_SERVER_NULL);
return;
}
//验证失败
try {
int result = VaptchaUtil.postJSON(server, token);
if(result != 1){
failResult(response, ResultCode.VAPTCHA_CHECK_FAIL);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
//如果token、server值不为空,且验证成功,则进行下一个过滤器
filterChain.doFilter(request, response);
}
//返回错误信息
private void failResult(HttpServletResponse response, ResultCode rc){
//返回json数据
JsonResult jsonResult = ResultTool.fail(rc);
//处理编码方式,防止中文乱码的情况
response.setContentType("text/json;charset=utf-8");
//塞到HttpServletResponse中返回给前台
try {
response.getWriter().write(JSON.toJSONString(jsonResult));
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.加入配置
//添加vaptcha登录验证过滤器
http.addFilterBefore(vaptchaFilterr(), UsernamePasswordAuthenticationFilter.class);
---------------------------------------------------------
/**
* 添加 vaptcha验证过滤器
* @return
*/
@Bean
public VaptchaFilterr vaptchaFilterr(){
return new VaptchaFilterr();
}
3、注册
1.创建实体类
@Data
@ToString
@ApiModel(value = "用户注册")
public class RegisterUser implements Serializable {
/**
* 用户名
*/
@ApiModelProperty(value = "用户名")
private String userName;
/**
* 账号
*/
@ApiModelProperty(value = "账号")
private String account;
/**
* 密码
*/
@ApiModelProperty(value = "密码")
private String password;
/**
* vaptcha验证token
*/
@ApiModelProperty(value = "vaptcha验证token")
private String token;
/**
* vaptcha验证server
*/
@ApiModelProperty(value = "vaptcha验证server")
private String server;
}
@Data
@ToString
@TableName("sys_user_role_relation")
public class UserAndRoleRelation implements Serializable {
private static final long serialVersionUID = 915478504870211231L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "user_id")
private Integer userId;
@TableField(value = "role_id")
private Integer roleId;
}
2.mapper
/**
* 增加
* @param roleAndPermision
* @return
*/
@Override
int insert(UserAndRoleRelation roleAndPermision);
xml
<mapper namespace="com.lunz.securityproject.mapper.UserAndRoleRelationMapper">
<resultMap id="m" type="com.lunz.securityproject.entity.domain.UserAndRoleRelation">
<id column="id" jdbcType="INTEGER" property="id"></id>
<result column="user_id" jdbcType="INTEGER" property="userId"></result>
<result column="role_id" jdbcType="INTEGER" property="roleId"></result>
</resultMap>
<insert id="insert" parameterType="UserAndRoleRelation">
insert into sys_user_role_relation(user_id,role_id) value (#{userId},#{roleId})
</insert>
</mapper>
3.service
/**
* 用户注册
* @param user
* @return
*/
int insert(RegisterUser user);
4.impl
@Override
@Transactional(rollbackFor = Exception.class)
public int insert(RegisterUser user) {
//判断账号是否重复
int count = service.lambdaQuery().select(SysUser :: getId).eq(SysUser :: getAccount, user.getAccount()).count();
if(count != 0){
return 0;
}
//增加用户
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
SysUser sysUser = new SysUser();
BeanUtils.copyProperties(user, sysUser);
sysUser.setPassword(bCryptPasswordEncoder.encode(sysUser.getPassword()));
sysUser.setLastLoginTime(new Date());
sysUser.setEnabled(true);
sysUser.setAccountNonExpired(true);
sysUser.setAccountNonLocked(true);
sysUser.setCredentialsNonExpired(true);
sysUser.setCreateTime(new Date());
sysUser.setUpdateTime(new Date());
baseMapper.insert(sysUser);
//获取新增用户id并在用户-角色表中增加记录
SysUser u = service.lambdaQuery().select(SysUser :: getId).eq(SysUser :: getAccount, sysUser.getAccount()).one();
UserAndRoleRelation userAndRoleRelation = new UserAndRoleRelation();
userAndRoleRelation.setUserId(u.getId());
userAndRoleRelation.setRoleId(2);
return userAndRoleRelationMapper.insert(userAndRoleRelation);
}
5.controller
@PostMapping("/register")
public JsonResult register(RegisterUser user) throws Exception{
//因为验证需要表单格式的数据而不是JSON数据,所以不需要加@RequestBody注解
if(user == null){
return ResultTool.fail(ResultCode.PARAM_IS_BLANK);
}else{
//开始注册
int result = sysUserService.insert(user);
if(result > 0){
return ResultTool.success();
}else if(result == 0){
return ResultTool.fail(ResultCode.USER_ACCOUNT_ALREADY_EXIST);
}else{
return ResultTool.fail();
}
}
}
4、修改密码
1.数据库中加入url
insert into sys_request_path(url,description) value('/user/updatePassword','修改密码');
insert into sys_request_path_permission_relation(url_id,permission_id) value(2,4);
2.创建实体类
@Getter
@Setter
@ToString
public class UpdatePassword {
private String account;
private String oldPassword;
private String newPassword;
}
3.mapper
/**
* 修改密码
* @param updatePassword
* @return
*/
int updatePassword(UpdatePassword updatePassword);
xml:
<update id="updatePassword" parameterType="updatePassword">
update sys_user set password = #{newPassword} where account = #{account}
</update>
4.service
/**
* 修改密码
* @param account
* @return
*/
int updatePassword(String account, UpdatePassword updatePassword);
5.impl
@Override
public int updatePassword(String account, UpdatePassword updatePassword) {
//获取数据库中的账号密码
SysUser sysUser = service.lambdaQuery().select(SysUser::getAccount, SysUser :: getPassword).eq(SysUser::getAccount, account).one();
//oldPassword为数据库中存的密码
String oldPassword = sysUser.getPassword();
//调用matches方法将未加密的密码与经过加密的密码比对是否相同(第一个参数为未加密密码,第二个为加密密码)
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
boolean b = bCryptPasswordEncoder.matches(updatePassword.getOldPassword(), oldPassword);
//密码正确则修改
if(b){
updatePassword.setAccount(account);
updatePassword.setNewPassword(bCryptPasswordEncoder.encode(updatePassword.getNewPassword()));
return baseMapper.updatePassword(updatePassword);
}
return -1;
}
6.controller
@Autowired
private SessionRegistry sessionRegistry;
-------------------------------------------------------------------------
@PostMapping("/updatePassword")
public JsonResult updatePassword(Authentication authentication, UpdatePassword updatePassword){
String account = authentication.getName();
int result = sysUserService.updatePassword(account,updatePassword);
if(result == -1){
return ResultTool.fail(USER_CREDENTIALS_ERROR);
}
if(result > 0){
//修改成功,踢出用户,让用户重新登录
kickOutUser(account);
return ResultTool.success();
}
return ResultTool.fail();
}
--------------------------------------------------------------------------
/**
* 踢出指定用户
* @param account
*/
private void kickOutUser(String account){
//获取session中所有用户信息
List<Object> users = sessionRegistry.getAllPrincipals();
for(Object principal : users ){
//判断principal是否为 User类
if(principal instanceof User){
//获取用户名
String principalName = ((User)principal).getUsername();
//与传入的用户名比对
if(principalName.equals(account)){
//获取session,获取该principal上的所有session
List<SessionInformation> sessionInformations = sessionRegistry.getAllSessions(principal, false);
if(sessionInformations != null && sessionInformations.size() > 0){
for(SessionInformation sessionInformation : sessionInformations){
//将session改成过期
sessionInformation.expireNow();
}
}
}
}
}
}
7.踢出用户配置
//启动会话管理
.and().sessionManagement()
//设置最大登录数为1
.maximumSessions(1)
//当登录数达最大值时,是否保留已登录用户(true:保留,新用户无法登录;false:不保留,踢出旧用户)
.maxSessionsPreventsLogin(false)
//处理账号失效、被挤下线
.expiredSessionStrategy(sessionInformationExpiredStrategy)
//踢出用户配置
.sessionRegistry(sessionRegistry());
-----------------------------------------------------
/**
* 添加踢出用户配置
* @return
*/
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
5、注销用户
1.数据库中加入url
insert into sys_request_path(url,description) value('/user/deleteUser','删除账号');
insert into sys_request_path_permission_relation(url_id,permission_id) value(3,3);
2.mapper
SysUserMapper:
/**
* 删除用户
* @param account
* @return
*/
int deleteUser(String account);
UserAndRoleRelationMapper:
/**
* 通过account获取id
* @param account
* @return
*/
int getIdByAccount(String account);
xml:
SysUserMapper.xml:
<delete id="deleteUser" parameterType="string">
delete from sys_user where account = #{account}
</delete>
UserAndRoleRelationMapper.xml:
<select id="getIdByAccount" parameterType="String" resultType="int">
select id from sys_user_role_relation
where user_id = (select id from sys_user where account = #{account})
</select>
3.service
/**
* 删除用户
* @param account
* @return
*/
int deleteUser(String account);
4.impl
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteUser(String account) {
//删除用户-角色表中的记录
int id = userAndRoleRelationMapper.getIdByAccount(account);
userAndRoleRelationMapper.deleteById(id);
return baseMapper.deleteUser(account);
}
5.controller
@DeleteMapping("/deleteUser")
public JsonResult deleteUser(Authentication authentication){
String account = authentication.getName();
if(account == null){
return ResultTool.fail(PARAM_IS_BLANK);
}
int result = sysUserService.deleteUser(account);
if(result > 0){
//注销成功,退回到未登录状态
kickOutUser(account);
return ResultTool.success();
}
return ResultTool.fail();
}