网站开发的功能需求文档,品牌vi设计手册ppt,福州市高速公路建设指挥部网站,宝塔怎么做第二个网站资料来源于 SpringSecurity框架教程-Spring SecurityJWT实现项目级前端分离认证授权
侵权删
目录
介绍
快速开始
认证
认证流程
登录校验流程
SpringSecurity完整流程
认证流程详解
代码实现
准备工作
mysql
mybatis-plus
redis
统一返回类 核心代码
密码加密存…资料来源于 SpringSecurity框架教程-Spring SecurityJWT实现项目级前端分离认证授权
侵权删
目录
介绍
快速开始
认证
认证流程
登录校验流程
SpringSecurity完整流程
认证流程详解
代码实现
准备工作
mysql
mybatis-plus
redis
统一返回类 核心代码
密码加密存储
自定义登录接口
认证过滤器
退出登录
测试
授权
权限系统的作用
授权基本流程
授权实现
限制访问资源所需权限
封装权限信息
从数据库查询权限信息
RBAC权限模型
自定义失败处理
跨域 更多细节
其它权限校验方法
hasAuthority方法执行的源码
其他权限校验方法
自定义权限校验方法
基于配置的权限控制
CSRF
认证成功处理器
认证失败处理器
登出成功处理器 介绍
Spring Security 是一个功能强大且灵活的身份验证和访问控制框架用于保护基于 Java 的企业应用程序。它提供了全面的安全解决方案包括身份认证、授权、攻击防范、会话管理等功能可以帮助开发者构建安全可靠的应用程序。
以下是 Spring Security 的一些主要特性和用途 身份认证AuthenticationSpring Security 支持多种身份认证方式包括基于表单、HTTP 基本认证、HTTP Digest 认证、OpenID、OAuth 等。开发者可以根据应用程序的需求选择合适的认证方式。 授权AuthorizationSpring Security 提供了灵活的授权机制可以基于角色Role、权限Permission、表达式Expression等对用户进行访问控制。开发者可以根据应用程序的权限模型来定义授权规则确保用户只能访问其具有权限的资源。 攻击防范Spring Security 集成了各种安全防护机制包括防止 CSRF跨站请求伪造、点击劫持、会话固定攻击、SQL 注入、XSS跨站脚本攻击等常见攻击。开发者可以通过配置简单的安全策略来保护应用程序免受这些攻击。 会话管理Spring Security 提供了对用户会话的管理功能包括基于内存、基于数据库、基于集群的会话管理等。开发者可以灵活地配置会话管理策略确保用户会话的安全性和可靠性。 记住我Remember MeSpring Security 支持“记住我”功能允许用户在下次访问应用程序时自动登录而无需重新输入用户名和密码。 集成性Spring Security 可以与 Spring 框架及其他常见的 Java Web 框架如 Spring Boot、Spring MVC、Spring WebFlux无缝集成为开发者提供方便易用的安全解决方案。
总之Spring Security 是一个功能强大、灵活且易于使用的安全框架为 Java 开发者提供了全面的安全解决方案帮助他们构建安全可靠的企业级应用程序。
而认证和授权也是SpringSecurity作为安全框架的核心功能本文主要介绍SpringSecurity中认证和授权的基本操作。
快速开始
我们先简单实现一个基于SpringBoot2框架的SpringSecurity项目。
第1步先在springboot项目的pom文件夹中加入SpringSecurity的依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-security/artifactId/dependency
第2步创建一个后台接口
RestController
RequestMapping(/hello)
public class TestController {GetMappingpublic String hello(){System.out.println(hello);return hello;}
}
到这里SpringSecurity的简单实现就完成了是不是特别简单
然后我们尝试去访问后台的接口就会自动跳转到一个SpringSecurity的默认登陆页面默认用户名是user,密码会输出在控制台我们必须登陆之后才能对接口进行访问。
在浏览器输入localhost:7000/hello如果没有登录过就会被SpringSecurity拦截跳到登录页面 用户名默认为user密码在控制台中给出 输入用户名密码之后才能正确访问我们自己定义的接口。
认证
认证流程
登录校验流程 SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。 图中只展示了核心过滤器其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要由它负责。
ExceptionTranslationFilter处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor负责权限校验的过滤器。
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。 认证流程详解 Authentication接口: 它的实现类表示当前访问系统的用户封装了用户相关信息。
AuthenticationManager接口定义了认证Authentication的方法authenticate()
UserDetailsService接口加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法SpringSecurity初始是从内存中获取的这个接口后面需要我们实现去自己的数据库中获取。
UserDetails接口提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中这个也是需要我们自己实现的。 代码实现
准备工作
mysql
从之前的分析我们可以知道我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码。
创建一个数据库表
DROP TABLE IF EXISTS user;
CREATE TABLE user (id int(11) NOT NULL COMMENT 用户Id,name varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 用户名称,password varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 用户密码,phone varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 用户手机号码,email varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 用户邮箱,PRIMARY KEY (id) USING BTREE
) ENGINE InnoDB CHARACTER SET utf8 COLLATE utf8_general_ci ROW_FORMAT Dynamic;在pom文件中引入对应依赖 dependencygroupIdcom.mysql/groupIdartifactIdmysql-connector-j/artifactId/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency
在配置文件中配置数据库信息
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://xxxxxx:3306/testusername: xxxxpassword: xxxx
定义实体类
Data
AllArgsConstructor
NoArgsConstructor
ToString
public class User implements Serializable {private Integer id;private String name;private String password;private String phone;private String email;
}
mybatis-plus
导入依赖
propertiesmybatis-plus.version3.5.3.1/mybatis-plus.version
/properties
dependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion${mybatis-plus.version}/version
/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-generator/artifactIdversion${mybatis-plus.version}/version
/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-extension/artifactIdversion${mybatis-plus.version}/version
/dependency
使用
定义mapper
Mapper
public interface UserMapper extends BaseMapperUser {
}定义service
public interface UserService extends IServiceUser {}
定义serviceImpl
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService{}
redis
导入依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactId/dependency
在配置文件中配置redis连接信息
spring:redis:host: 47.115.217.159 #默认端口是6379就可以不写
重写redis的序列化器
//redis的序列化器
public class FastJsonRedisSerializerT implements RedisSerializerT
{public static final Charset DEFAULT_CHARSET Charset.forName(UTF-8);private ClassT clazz;static{ParserConfig.getGlobalInstance().setAutoTypeSupport(true);}public FastJsonRedisSerializer(ClassT clazz){super();this.clazz clazz;}Overridepublic byte[] serialize(T t) throws SerializationException{if (t null){return new byte[0];}return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);}Overridepublic T deserialize(byte[] bytes) throws SerializationException{if (bytes null || bytes.length 0){return null;}String str new String(bytes, DEFAULT_CHARSET);return JSON.parseObject(str, clazz);}protected JavaType getJavaType(Class? clazz){return TypeFactory.defaultInstance().constructType(clazz);}
}形式参数可能会爆红不用管他可以运行
Configuration
public class RedisConfig {BeanSuppressWarnings(value { unchecked, rawtypes })public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory connectionFactory){RedisTemplateObject, Object template new RedisTemplate();template.setConnectionFactory(connectionFactory);FastJsonRedisSerializer serializer new FastJsonRedisSerializer(Object.class);// 使用StringRedisSerializer来序列化和反序列化redis的key值template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(serializer);// Hash的key也采用StringRedisSerializer的序列化方式template.setHashKeySerializer(new StringRedisSerializer());template.setHashValueSerializer(serializer);template.afterPropertiesSet();return template;}
}
统一返回类
public class RT {/*** 状态码*/private Integer code;/*** 提示信息如果有错误时前端可以获取该字段进行提示*/private String msg;/*** 查询到的结果数据*/private T data;public R(Integer code, String msg) {this.code code;this.msg msg;}public R(Integer code, T data) {this.code code;this.data data;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg msg;}public T getData() {return data;}public void setData(T data) {this.data data;}public R(Integer code, String msg, T data) {this.code code;this.msg msg;this.data data;}
}核心代码
创建一个类实现UserDetailsService接口重写其中的方法。根据用户名从数据库中查询用户信息
Service
public class UserDetailServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取用户信息User user userMapper.selectOne(new LambdaQueryWrapperUser().eq(User::getName, username));if(Objects.isNull(user)){throw new RuntimeException(用户不存在);}//权限信息//封装成UserDetails对象返回LoginUser loginUser new LoginUser(user);return loginUser;}
}
因为UserDetailsService方法的返回值是UserDetails类型所以需要定义一个类实现该接口把用户信息封装在其中。
Data
AllArgsConstructor
NoArgsConstructor
ToString
public class LoginUser implements UserDetails {private User user;//返回用户的权限信息Overridepublic Collection? extends GrantedAuthority getAuthorities() {return null;}//返回用户的密码信息Overridepublic String getPassword() {return user.getPassword();}//返回用户的用户名Overridepublic String getUsername() {return user.getName();}//用户的帐户是否未过期Overridepublic boolean isAccountNonExpired() {return true;}//用户的帐户是否未被锁定Overridepublic boolean isAccountNonLocked() {return true;}//用户的凭据密码是否未过期Overridepublic boolean isCredentialsNonExpired() {return true;}//用户是否启用Overridepublic boolean isEnabled() {return true;}
}注意如果要测试需要往用户表中写入用户数据并且如果你想让用户的密码是明文存储需要在密码前加{noop}。例如 这样登陆的时候就可以用“李四”作为用户名1234作为密码来登陆了。
不过我们往数据库中存储密码肯定不会以明文形式存储的后面我们会使用MD5加密的方式也就不需要{noop}了。
密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
默认使用的PasswordEncoder要求数据库中的密码格式为{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中SpringSecurity就会使用该PasswordEncoder来进行密码校验。
我们可以定义一个SpringSecurity的配置类给容器中注入一个PasswordEncoder 的组件即可
Configuration
public class SecurityConfig {Beanpublic PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}}
自定义登录接口
接下我们需要自定义登陆接口然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。形式参数可能会爆红不用管他运行没问题的
Configuration
public class SecurityConfig {Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}}
认证成功的话要生成一个jwt放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户我们需要把用户信息存入redis可以把用户id作为key。
封装对应的JwtUtils
导入依赖 dependencygroupIdcom.auth0/groupIdartifactIdjava-jwt/artifactIdversion4.3.0/version/dependency
编写配置类
public class JwtUtil {private static final String KEY 随便写;//接收业务数据,生成token并返回public static String genToken(MapString, Object claims) {return JWT.create().withClaim(claims, claims).withExpiresAt(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 12)).sign(Algorithm.HMAC256(KEY));}//接收token,验证token,并返回业务数据public static MapString, Object parseToken(String token) {return JWT.require(Algorithm.HMAC256(KEY)).build().verify(token).getClaim(claims).asMap();}}
编写用户登录的controller类
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;/*** 用户登录* param user* return*/PostMapping(/login)public R UserLogin(RequestBody User user){return userService.userLogin(user);}
}
service实现类
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService{Autowiredprivate AuthenticationManager authenticationManager;Autowiredprivate RedisTemplate redisTemplate;Overridepublic R userLogin(User user) {UsernamePasswordAuthenticationToken authenticationTokennew UsernamePasswordAuthenticationToken(user.getName(),user.getPassword());Authentication authenticate authenticationManager.authenticate(authenticationToken);if(Objects.isNull(authenticate)){throw new RuntimeException(用户名或密码错误);}//使用userId生成tokenLoginUser loginUser (LoginUser) authenticate.getPrincipal();String id loginUser.getUser().getId().toString();String token JwtUtil.genToken(new HashMap() {{put(id, id);}});//将用户信息存入redisredisTemplate.opsForValue().set(loginUser:id,loginUser);//将携带token的值返回给前端MapString,String mapnew HashMap(){{put(token,token);}};return new R(200,登录成功,map);}
}
认证过滤器
我们需要自定义一个过滤器这个过滤器会去获取请求头中的token对token进行解析取出其中的userid。
使用userid去redis中获取对应的LoginUser对象。
然后封装Authentication对象存入SecurityContextHolder
/*** 记得在security的配置文件中将这个过滤器配置在UsernamePasswordAuthenticationFilter之前执行*/
Configuration
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {Autowiredprivate RedisTemplate redisTemplate;Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {//先从请求头中获取tokenString token request.getHeader(token);/**如果token不存在说明用户发请求时没有携带token那么可能就是用户没有登录过滤器就放行* 交给后面的FilterSecurityInterceptor进行处理**/if (StringUtils.isEmpty(token)) {filterChain.doFilter(request, response);//这里一定要return因为在返回结果的时候也会来到这里如果没有return就会继续执行下面的代码return;}//解析token得到token信息去redis中获取信息MapString, Object map JwtUtil.parseToken(token);String id map.get(id).toString();LoginUser loginUser;try {loginUser (LoginUser) redisTemplate.opsForValue().get(loginUser: id);if(Objects.isNull(loginUser)){//运行到这里说明redis中不存在对应的用户信息抛出一个错误throw new RuntimeException(用户不存在);}} catch (Exception e) {//运行到这里说明redis中不存在对应的用户信息抛出一个错误throw new RuntimeException(用户不存在);}//将用户信息存入SecurityContextHolder/*** TODO 获取权限信息封装到Authentication中* 如果过滤器执行到这里说明前端携带了token并且redis中也存在对应的信息说明这个用户是已经登录过的所以是已认证状态* 如果是已认证状态UsernamePasswordAuthenticationToken(loginUser,null,null)这里第一个参数是用户信息,* 第三个参数是权限信息*/UsernamePasswordAuthenticationToken authenticationToken new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authenticationToken);//放行filterChain.doFilter(request, response);}
}
在SpringSecurity的过滤器链中将我们自己写的过滤器加进去并且要在用户名密码校验器之前。
在SpringSecurity的配置文件中配置 Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 关闭csrf.csrf().disable()//不通过Session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests().antMatchers(/user/login).anonymous()//未登录才能访问这个请求.anyRequest().authenticated();/*** 将JwtAuthenticationTokenFilter过滤器配置到security过滤器链之中并且在UsernamePasswordAuthenticationFilter* 之前执行*/http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);return http.build();}
退出登录
对于退出登录功能我们只需要定义一个登陆接口然后获取SecurityContextHolder中的认证信息删除redis中对应的数据即可。
Service
public class UserServiceImpl extends ServiceImplUserMapper, User implements UserService{Autowiredprivate AuthenticationManager authenticationManager;Autowiredprivate RedisTemplate redisTemplate;Overridepublic R userLogout() {//再security的context中获取当前登录的用户Authentication authentication SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();//删除redis中对应的数据redisTemplate.delete(loginUser:loginUser.getUser().getId());return new R(200,退出成功);}
}
测试
我这里准备了三个请求
登录请求 hello请求 退出登录请求 首先在没有登录的情况下发送hello请求 发送登录请求 将token加入hello请求的header中再次发送 将token加入到logout请求的header中发送请求 退出之后再次写到token发送hello请求 ok到这里认证流程就走完了。 授权
权限系统的作用
比如一个学校图书馆的管理系统如果是普通学生登录就能看到借书还书相关的功能不可能让他看到并且去使用添加书籍信息删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了应该就能看到并使用添加书籍信息删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样如果有人知道了对应功能的接口地址就可以不通过前端直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断判断当前用户是否有相应的权限必须具有所需权限才能进行相应的操作。
授权基本流程
在SpringSecurity中会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication然后设置我们的资源所需要的权限即可。
授权实现
限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
SpringBootApplication
//开启限制访问资源权限
EnableGlobalMethodSecurity(prePostEnabled true)
public class Application {public static void main(String[] args) {ConfigurableApplicationContext run SpringApplication.run(Application.class, args);System.out.println(run);}}
然后就可以使用对应的注解。PreAuthorize GetMapping//用户具有 admin 权限才能访问这个接口PreAuthorize(hasAuthority(admin))public String hello(){System.out.println(hello);return hello;}
封装权限信息
我们前面在写UserDetailsServiceImpl的时候说过在查询出用户后还要获取对应的权限信息封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。
我们之前定义了UserDetails的实现类LoginUser想要让其能封装权限信息就要对其进行修改
LoginUser.java中添加或修改如下代码
private ListString permission;/*** 加上这个注解表示authorities这个属性不会进行序列化* 因为我们后面需要将loginUser的对象序列化之后存储到redis中* 但是对于GrantedAuthority类型的对象进行序列化会报错所以排除这个属性*/JSONField(serialize false)private ListGrantedAuthority authorities;public LoginUser(User user, ListString permission) {this.user user;this.permission permission;}//返回用户的权限信息Overridepublic Collection? extends GrantedAuthority getAuthorities() {//ctralt ---查询一个接口的事项类/*** 因为每一次框架内部调用这个方法都要进行转化不如将authorities变成一个成员变量并且加一个判断* 不为空的时候直接返回即可为空时再进行转化*/if(!Objects.isNull(authorities)){return authorities;}/*** 把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中*/authorities permission.stream().map((item) - {GrantedAuthority authority new SimpleGrantedAuthority(item);return authority;}).collect(Collectors.toList());return authorities;}
在UserServiceDetailService.java中修改成如下代码
Service
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapperUser wrapper new LambdaQueryWrapper();wrapper.eq(User::getUserName,username);User user userMapper.selectOne(wrapper);if(Objects.isNull(user)){throw new RuntimeException(用户名或密码错误);}//根据用户查询权限信息 添加到LoginUser中这里先写死ListString list new ArrayList(Arrays.asList(test));return new LoginUser(user,list);}
}
然后进行测试第一次测试访问hello接口肯定是访问不到的因为我们给用户设置的权限是testhello接口需要admin权限才能请求。 把用户的权限设置成admin就可以访问成功
Service
public class UserDetailsServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {LambdaQueryWrapperUser wrapper new LambdaQueryWrapper();wrapper.eq(User::getUserName,username);User user userMapper.selectOne(wrapper);if(Objects.isNull(user)){throw new RuntimeException(用户名或密码错误);}//根据用户查询权限信息 添加到LoginUser中这里先写死ListString list new ArrayList(Arrays.asList(admin));return new LoginUser(user,list);}
} 在实际应用中用户的权限应该从数据库中获取所以就引出了RBAC模型。
从数据库查询权限信息
RBAC权限模型
RBAC权限模型Role-Based Access Control即基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。 在数据库中定义这些表。学习阶段的话也不用把表属性设置的太复杂保留主要字段就行下面的就比较复杂
/*Table structure for table sys_menu */DROP TABLE IF EXISTS sys_menu;CREATE TABLE sys_menu (id bigint(20) NOT NULL AUTO_INCREMENT,menu_name varchar(64) NOT NULL DEFAULT NULL COMMENT 菜单名,path varchar(200) DEFAULT NULL COMMENT 路由地址,component varchar(255) DEFAULT NULL COMMENT 组件路径,visible char(1) DEFAULT 0 COMMENT 菜单状态0显示 1隐藏,status char(1) DEFAULT 0 COMMENT 菜单状态0正常 1停用,perms varchar(100) DEFAULT NULL COMMENT 权限标识,icon varchar(100) DEFAULT # COMMENT 菜单图标,create_by bigint(20) DEFAULT NULL,create_time datetime DEFAULT NULL,update_by bigint(20) DEFAULT NULL,update_time datetime DEFAULT NULL,del_flag int(11) DEFAULT 0 COMMENT 是否删除0未删除 1已删除,remark varchar(500) DEFAULT NULL COMMENT 备注,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8mb4 COMMENT菜单表;/*Table structure for table sys_role */DROP TABLE IF EXISTS sys_role;CREATE TABLE sys_role (id bigint(20) NOT NULL AUTO_INCREMENT,name varchar(128) DEFAULT NULL,role_key varchar(100) DEFAULT NULL COMMENT 角色权限字符串,status char(1) DEFAULT 0 COMMENT 角色状态0正常 1停用,del_flag int(1) DEFAULT 0 COMMENT del_flag,create_by bigint(200) DEFAULT NULL,create_time datetime DEFAULT NULL,update_by bigint(200) DEFAULT NULL,update_time datetime DEFAULT NULL,remark varchar(500) DEFAULT NULL COMMENT 备注,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT3 DEFAULT CHARSETutf8mb4 COMMENT角色表;/*Table structure for table sys_role_menu */DROP TABLE IF EXISTS sys_role_menu;CREATE TABLE sys_role_menu (role_id bigint(200) NOT NULL AUTO_INCREMENT COMMENT 角色ID,menu_id bigint(200) NOT NULL DEFAULT 0 COMMENT 菜单id,PRIMARY KEY (role_id,menu_id)
) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8mb4;/*Table structure for table sys_user */DROP TABLE IF EXISTS sys_user;CREATE TABLE sys_user (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键,user_name varchar(64) NOT NULL DEFAULT NULL COMMENT 用户名,nick_name varchar(64) NOT NULL DEFAULT NULL COMMENT 昵称,password varchar(64) NOT NULL DEFAULT NULL COMMENT 密码,status char(1) DEFAULT 0 COMMENT 账号状态0正常 1停用,email varchar(64) DEFAULT NULL COMMENT 邮箱,phonenumber varchar(32) DEFAULT NULL COMMENT 手机号,sex char(1) DEFAULT NULL COMMENT 用户性别0男1女2未知,avatar varchar(128) DEFAULT NULL COMMENT 头像,user_type char(1) NOT NULL DEFAULT 1 COMMENT 用户类型0管理员1普通用户,create_by bigint(20) DEFAULT NULL COMMENT 创建人的用户id,create_time datetime DEFAULT NULL COMMENT 创建时间,update_by bigint(20) DEFAULT NULL COMMENT 更新人,update_time datetime DEFAULT NULL COMMENT 更新时间,del_flag int(11) DEFAULT 0 COMMENT 删除标志0代表未删除1代表已删除,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT3 DEFAULT CHARSETutf8mb4 COMMENT用户表;/*Table structure for table sys_user_role */DROP TABLE IF EXISTS sys_user_role;CREATE TABLE sys_user_role (user_id bigint(200) NOT NULL AUTO_INCREMENT COMMENT 用户id,role_id bigint(200) NOT NULL DEFAULT 0 COMMENT 角色id,PRIMARY KEY (user_id,role_id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;根据用户ID查询用户权限的sql语句
SELECT DISTINCT m.perms
FROMsys_user_role urLEFT JOIN sys_role r ON ur.role_id r.idLEFT JOIN sys_role_menu rm ON ur.role_id rm.role_idLEFT JOIN sys_menu m ON m.id rm.menu_id
WHEREuser_id 2AND r.status 0AND m.status 0
然后在代码中添加Menu对应的实体类以及Mapper文件在UserDetailServiceImpl.java中把我们定死的那个权限修改成从数据库中查询即可其他地方不需要改变。
Menu实体类我在上面的sql建表语句中删除了一些字段所以这里的实体类中的属性跟上面的sql建表语句中的对不上
TableName(sys_menu)
Data
AllArgsConstructor
NoArgsConstructor
public class Menu implements Serializable {private static final long serialVersionUID -54979041104113736L;TableIdprivate Integer id;private String menuName;private String status;private String perms;private String delFlag;private String remark;}MenuMapper.java
Mapper
public interface MenuMapper extends BaseMapperMenu {ListString selectPermsByUserId(Long id);
}
MenuMapper.xml
?xml version1.0 encodingUTF-8 ?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.itheima.mapper.MenuMapperselect idselectPermsByUserId resultTypejava.lang.StringSELECTDISTINCT m.permsFROMsys_user_role urLEFT JOIN sys_role r ON ur.role_id r.idLEFT JOIN sys_role_menu rm ON ur.role_id rm.role_idLEFT JOIN sys_menu m ON m.id rm.menu_idWHEREuser_id #{userid}AND r.status 0AND m.status 0/select/mapper
UserDetalisService.java
Service
public class UserDetailServiceImpl implements UserDetailsService {Autowiredprivate UserMapper userMapper;Autowiredprivate MenuMapper menuMapper;Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//获取用户信息User user userMapper.selectOne(new LambdaQueryWrapperUser().eq(User::getName, username));if(Objects.isNull(user)){throw new RuntimeException(用户不存在);}//权限信息ListString list menuMapper.selectPermsByUserId(user.getId().longValue());//封装成UserDetails对象返回LoginUser loginUser new LoginUser(user,list);return loginUser;}
}
自定义失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以如果我们需要自定义异常处理我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
实现方式如下
先引入一个工具类用来渲染响应的数据格式
public class WebUtils
{/*** 将字符串渲染到客户端** param response 渲染对象* param string 待渲染的字符串* return null*/public static String renderString(HttpServletResponse response, String string) {try{response.setStatus(200);response.setContentType(application/json);response.setCharacterEncoding(utf-8);response.getWriter().print(string);}catch (IOException e){e.printStackTrace();}return null;}
}
Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {R result new R(HttpStatus.FORBIDDEN.value(), 权限不足);String json JSON.toJSONString(result);WebUtils.renderString(response,json);}
}
然后自定义实现类
//自定义授权失败处理
Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {R result new R(HttpStatus.FORBIDDEN.value(), 权限不足);String json JSON.toJSONString(result);WebUtils.renderString(response,json);}
}
//自定义失败处理
Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {ResponseResult result new ResponseResult(HttpStatus.UNAUTHORIZED.value(), 认证失败请重新登录);String json JSON.toJSONString(result);WebUtils.renderString(response,json);}
}
然后将这两个自定义实现类注入到Security的配置文件中 Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//其他配置............................................//添加自定义异常处理器http.exceptionHandling().accessDeniedHandler(accessDeniedHandler).authenticationEntryPoint(authenticationEntryPoint);return http.build();}跨域
浏览器出于安全的考虑使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略否则就是跨域的HTTP请求默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信即协议、域名、端口号都完全一致。
前后端分离项目前端项目和后端项目一般都不是同源的所以肯定会存在跨域请求的问题。
所以我们就要处理一下让前端能进行跨域请求。
对于springboot的跨域处理大家可以去看我的这一篇文章
springboot中解决CORS的几种方式
这里可以使用这一种方式
Configuration
public class CorsConfig implements WebMvcConfigurer {Overridepublic void addCorsMappings(CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping(/**)// 设置允许跨域请求的域名.allowedOriginPatterns(*)// 是否允许cookie.allowCredentials(true)// 设置允许的请求方式.allowedMethods(GET, POST, DELETE, PUT)// 设置允许的header属性.allowedHeaders(*)// 跨域允许时间.maxAge(3600);}
}
然后再Security的配置文件中开启CORS
Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {//其他内容...............................http.cors();return http.build();} 更多细节
其它权限校验方法
我们前面都是使用PreAuthorize注解然后在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如hasAnyAuthorityhasRolehasAnyRole等。
这里我们先不急着去介绍这些方法我们先去理解hasAuthority的原理然后再去学习其他方法你就更容易理解而不是死记硬背区别。并且我们也可以选择定义校验方法实现我们自己的校验逻辑。
hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority大家只要断点调试既可知道它内部的校验原理。
它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
hasAuthority方法执行的源码
首先shiftshift点进搜索框输入SecurityExpressionRoot进入这个类 找到hasAuthority方法打上端点启动Security项目进行调试 hasAuthority方法中又调用了一个方法这个方法就在他下面 hasAnyAuthority hasAnyAuthority方法调用了hasAnyAuthorName这个方法我们继续追踪看看这个方法里面的逻辑 debug进去之后我们可以看到这个方法接收一个prefix以及多个字符串的roles我们在对应的接口方法中只校验一个这里自然也只有一个 然后这个方法会调用getAuthoritySet这个方法顾名思义这个方法就是去得到当前登录用户的权限继续追踪进这个方法 可以看到这里使用了authentication.getAuthorities()来得到登录用户的权限信息。
大家还记不记得我们前面在我们自定义的一个过滤器中定义了如下代码 再结合LoginUser中的这个方法 大家就是不是就知道是怎么获取用户的权限信息的了。
继续debug 得到了用户的权限信息 这里的权限信息是我们从数据库中查询得到了不要因为这里也是admin就跟上面接口的搞混了这两个不是同一个对象哈。
继续debug 这一步将上面的用户权限信息封装为一个Set集合
继续debug
回到前面的这个hasAnyAuthorityName 这里就来判断用户的权限Set集合中是否包含接口上定义的权限如果包含返回true
这里肯定是包含的所以就直接返回true校验就通过了当前用户就可以访问这个接口后面debug过程的就是一些处理过程与Security业务没什么关系这里就不继续进行追踪了。
其他权限校验方法
hasAnyAuthority方法可以传入多个权限只有用户有其中任意一个权限都可以访问对应资源。 PreAuthorize(hasAnyAuthority(admin,test,system:dept:list))public String hello(){return hello;} hasRole要求有对应的角色才可以访问但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。 PreAuthorize(hasRole(system:dept:list))public String hello(){return hello;} hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以。 PreAuthorize(hasAnyRole(admin,system:dept:list))public String hello(){return hello;}
自定义权限校验方法
我们也可以定义自己的权限校验方法在PreAuthorize注解中使用我们的方法。
Component(ex)
public class SGExpressionRoot {
public boolean hasAuthority(String authority){//获取当前用户的权限Authentication authentication SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser (LoginUser) authentication.getPrincipal();ListString permissions loginUser.getPermissions();//判断用户权限集合中是否存在authorityreturn permissions.contains(authority);}
}
在SPEL表达式中使用 ex相当于获取容器中bean的名字未ex的对象。然后再调用这个对象的hasAuthority方法 RequestMapping(/hello)PreAuthorize(ex.hasAuthority(system:dept:list))public String hello(){return hello;} 基于配置的权限控制 我们也可以在配置类中使用使用配置的方式对资源进行权限控制。 Overrideprotected void configure(HttpSecurity http) throws Exception {http.antMatchers(/testCors).hasAuthority(system:dept:list222)}
CSRF
CSRF是指跨站请求伪造Cross-site request forgery是web常见的攻击之一。
CSRF攻击与防御写得非常好_注销账号可以防范csrf吗-CSDN博客
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token而token并不是存储中cookie中并且需要前端代码去把token设置到请求头中才可以所以CSRF攻击也就不用担心了。
认证成功处理器
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候如果登录成功了是会调用AuthenticationSuccessHandler的方法进行认证成功后的处理的。AuthenticationSuccessHandler就是登录成功处理器。
我们也可以自己去自定义成功处理器进行成功后的相应处理。
Component
public class SGSuccessHandler implements AuthenticationSuccessHandler {
Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(认证成功了);}
}
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Autowiredprivate AuthenticationSuccessHandler successHandler;
Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().successHandler(successHandler);
http.authorizeRequests().anyRequest().authenticated();}
}
认证失败处理器
实际上在UsernamePasswordAuthenticationFilter进行登录认证的时候如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。AuthenticationFailureHandler就是登录失败处理器。
我们也可以自己去自定义失败处理器进行失败后的相应处理。
Component
public class SGFailureHandler implements AuthenticationFailureHandler {Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println(认证失败了);}
}
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Autowiredprivate AuthenticationSuccessHandler successHandler;
Autowiredprivate AuthenticationFailureHandler failureHandler;
Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// 配置认证成功处理器.successHandler(successHandler)
// 配置认证失败处理器.failureHandler(failureHandler);
http.authorizeRequests().anyRequest().authenticated();}
} 登出成功处理器
Component
public class SGLogoutSuccessHandler implements LogoutSuccessHandler {Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {System.out.println(注销成功);}
}
Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
Autowiredprivate AuthenticationSuccessHandler successHandler;
Autowiredprivate AuthenticationFailureHandler failureHandler;
Autowiredprivate LogoutSuccessHandler logoutSuccessHandler;
Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin()
// 配置认证成功处理器.successHandler(successHandler)
// 配置认证失败处理器.failureHandler(failureHandler);
http.logout()//配置注销成功处理器.logoutSuccessHandler(logoutSuccessHandler);
http.authorizeRequests().anyRequest().authenticated();}
} 结束........................................ 文章转载自: http://www.morning.mplld.cn.gov.cn.mplld.cn http://www.morning.ljjph.cn.gov.cn.ljjph.cn http://www.morning.nzsdr.cn.gov.cn.nzsdr.cn http://www.morning.mkfhx.cn.gov.cn.mkfhx.cn http://www.morning.nlgnk.cn.gov.cn.nlgnk.cn http://www.morning.sbjhm.cn.gov.cn.sbjhm.cn http://www.morning.zgdnd.cn.gov.cn.zgdnd.cn http://www.morning.ctrkh.cn.gov.cn.ctrkh.cn http://www.morning.rglp.cn.gov.cn.rglp.cn http://www.morning.srbmc.cn.gov.cn.srbmc.cn http://www.morning.lxthr.cn.gov.cn.lxthr.cn http://www.morning.tqdqc.cn.gov.cn.tqdqc.cn http://www.morning.lffgs.cn.gov.cn.lffgs.cn http://www.morning.znqztgc.cn.gov.cn.znqztgc.cn http://www.morning.tgts.cn.gov.cn.tgts.cn http://www.morning.mbpzw.cn.gov.cn.mbpzw.cn http://www.morning.nfsrs.cn.gov.cn.nfsrs.cn http://www.morning.hgsmz.cn.gov.cn.hgsmz.cn http://www.morning.flmxl.cn.gov.cn.flmxl.cn http://www.morning.ttryd.cn.gov.cn.ttryd.cn http://www.morning.ypqwm.cn.gov.cn.ypqwm.cn http://www.morning.mmxt.cn.gov.cn.mmxt.cn http://www.morning.yxwrr.cn.gov.cn.yxwrr.cn http://www.morning.zqdzg.cn.gov.cn.zqdzg.cn http://www.morning.kzqpn.cn.gov.cn.kzqpn.cn http://www.morning.qlbmc.cn.gov.cn.qlbmc.cn http://www.morning.gllgf.cn.gov.cn.gllgf.cn http://www.morning.hwbmn.cn.gov.cn.hwbmn.cn http://www.morning.pyncx.cn.gov.cn.pyncx.cn http://www.morning.hysqx.cn.gov.cn.hysqx.cn http://www.morning.kdldx.cn.gov.cn.kdldx.cn http://www.morning.frnjm.cn.gov.cn.frnjm.cn http://www.morning.znqztgc.cn.gov.cn.znqztgc.cn http://www.morning.zxybw.cn.gov.cn.zxybw.cn http://www.morning.kkhf.cn.gov.cn.kkhf.cn http://www.morning.blqgc.cn.gov.cn.blqgc.cn http://www.morning.sffwz.cn.gov.cn.sffwz.cn http://www.morning.zlkps.cn.gov.cn.zlkps.cn http://www.morning.hgbzc.cn.gov.cn.hgbzc.cn http://www.morning.zjcmr.cn.gov.cn.zjcmr.cn http://www.morning.trsdm.cn.gov.cn.trsdm.cn http://www.morning.xhddb.cn.gov.cn.xhddb.cn http://www.morning.mrkbz.cn.gov.cn.mrkbz.cn http://www.morning.kxscs.cn.gov.cn.kxscs.cn http://www.morning.gbqgr.cn.gov.cn.gbqgr.cn http://www.morning.ykrg.cn.gov.cn.ykrg.cn http://www.morning.lsjgh.cn.gov.cn.lsjgh.cn http://www.morning.ndtmz.cn.gov.cn.ndtmz.cn http://www.morning.yjknk.cn.gov.cn.yjknk.cn http://www.morning.jhxdj.cn.gov.cn.jhxdj.cn http://www.morning.rwls.cn.gov.cn.rwls.cn http://www.morning.fpngg.cn.gov.cn.fpngg.cn http://www.morning.qcsbs.cn.gov.cn.qcsbs.cn http://www.morning.smsjx.cn.gov.cn.smsjx.cn http://www.morning.pynzj.cn.gov.cn.pynzj.cn http://www.morning.lmhcy.cn.gov.cn.lmhcy.cn http://www.morning.tzzkm.cn.gov.cn.tzzkm.cn http://www.morning.ztmnr.cn.gov.cn.ztmnr.cn http://www.morning.dtmjn.cn.gov.cn.dtmjn.cn http://www.morning.tjpmf.cn.gov.cn.tjpmf.cn http://www.morning.zdmlt.cn.gov.cn.zdmlt.cn http://www.morning.twdwy.cn.gov.cn.twdwy.cn http://www.morning.krjyq.cn.gov.cn.krjyq.cn http://www.morning.ygztf.cn.gov.cn.ygztf.cn http://www.morning.ywrt.cn.gov.cn.ywrt.cn http://www.morning.fphbz.cn.gov.cn.fphbz.cn http://www.morning.pzbjy.cn.gov.cn.pzbjy.cn http://www.morning.hxxyp.cn.gov.cn.hxxyp.cn http://www.morning.gwdnl.cn.gov.cn.gwdnl.cn http://www.morning.lkmks.cn.gov.cn.lkmks.cn http://www.morning.wscfl.cn.gov.cn.wscfl.cn http://www.morning.xsgxp.cn.gov.cn.xsgxp.cn http://www.morning.mrcpy.cn.gov.cn.mrcpy.cn http://www.morning.dndjx.cn.gov.cn.dndjx.cn http://www.morning.ryfq.cn.gov.cn.ryfq.cn http://www.morning.hengqilan.cn.gov.cn.hengqilan.cn http://www.morning.rdpps.cn.gov.cn.rdpps.cn http://www.morning.fxzlg.cn.gov.cn.fxzlg.cn http://www.morning.ssjee.cn.gov.cn.ssjee.cn http://www.morning.kskpx.cn.gov.cn.kskpx.cn