文章目录
Spring Security 认证授权
作用
主要用于
登录
鉴权
快速入门
1,创建工程
2,引入依赖,直接创建引入
3,自带登录页面
注意
用户名默认为User,密码为一串md5数字,控制台打印出来
Using generated security password: d7d29b53-49f6-4e42-9895-6a2052d6bea1
修改的地方
1,修改登录页面
2,使用系统的用户名和密码
3,自带cookie/session,像自己生成jwt,无状态登录
4,前端页面携带jwt,放在请求头
5,鉴权操作
源码
是一些过滤器
有一个过滤器链,16个过滤器,其中有
登录流程
所以我们自己要实现自己的登录,从数据库中取出用户,就要实现UserDetailManager 的方法
认证过滤器
1,获取token
2,解析token
3,获取userid – 》 放入redis缓存
4,封装Authentication
5,存入SecurityContextHolder
登录
-
自定义登录接口 调用pridivermanager的auth方法
-
登录成功生成jwt
-
存入redis
-
自定义userdetailsmanager实现类
-
从数据库中获取系统用户
访问资源:自定义认证过滤器
也就是权限管理
-
获取token
-
从token中获取userid
-
从redis中通过userid获取用户信息
-
存SecurityContextHolder
-
-
jwt
json格式的token,一个无状态登录模型。
好处
- 不需要服务器端存session,cookie
特点
- 可以被看到信息,但是不可以修改,因为前面中要密钥,我们可以对其进行设置过期时间
构成
由三部分构成 abc.abc.abc,其中还有一个secrey密钥,修改就是要密钥,
-
头部
- 算法
- 类型 jwt
-
载荷
- 有效信息,比如name等等
-
签名
- 利用头的算法对前面两个进行加密
- 密钥
他会对传输的json进行base64编码
使用jwt
1,引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2,生成jwt
public static void main(String[] args) {
JwtBuilder jwtBuilder = Jwts.builder()
.setId("80")//设置ID
.setSubject("testJwt")//主题
.setIssuedAt(new Date())//签发日期
.setExpiration()// 设置过期时间
.signWith(SignatureAlgorithm.HS256, "miyao");//算法和密钥
String compact = jwtBuilder.compact();
System.out.println(compact);
//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4MCIsInN1YiI6InRlc3RKd3QiLCJpYXQiOjE2NjUwNDk3Mzl9.b6nO2WGLbdBjTbBWMerlCfaNL7xajmfHNZFYIHRy5SQ
//解密
Claims miyao = Jwts.parser().setSigningKey("miyao").parseClaimsJws(jwt).getBody();
System.out.println(miyao);
}
3,解密
Claims miyao = Jwts.parser().setSigningKey("miyao").parseClaimsJws(jwt).getBody();
System.out.println(miyao);
实现登录
1,定义配置类
- 密码自动加盐加密
- 放行登录接口,定义链
- 注入AuthenticationManager,在service层要使用他的auth方法进行验证
@Configuration
public class SecurityConfig {
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();//这里会自动加盐
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
return http.build();
}
//把验证的Manager放进去spring中,方便在service中调用auth方法
@Bean
public AuthenticationManager authenticationManager() throws Exception{
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
}
2,定义自己的类实现UserDetails来配套他的接口返回
@Data
@NoArgsConstructor
@AllArgsConstructor
//这个UserDetails就是就是把系统用户的信息转化为UserDetails供springSeriurty的UserDetailsService去使用的
public class LoginUser implements UserDetails {
private User user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
3,查询用户
要实现UserDetailsService,而UserDetailsManager是实现他的,所以我们直接实现UserDetailsService的loadUserByUsername方法,
来返回上面那个实现了UserDetails的实现类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户
User user = new User();
if (user == null){
throw new RuntimeException("用户不存在");
}
user.setUserId("111");
user.setUsername("hjh");
user.setPassword("");
return new LoginUser(user);
}
}
4,先service方法
- 用AuthenticationManager去调用ProviderManager的auth方法
- 从authenticate获取LoginUser放入redis
- 生成jwt给前端
public class LoginServiceImpl implements LoginService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
@Override
public ResponseResult login(User user) {
//3,使用ProviderManager auth方法进行验证
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(token);
//验证失败
if (authenticate == null){
throw new RuntimeException("用户名或者秘密错误");
}
//生成jwt给前端
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userId = loginUser.getUser().getUserId();
//4,生成jwt给前端
String jwt = JwtUtil.createJWT(userId);
HashMap<String,String> map = new HashMap<>();
map.put("token",jwt);
//5,把用户放入redis
redisCache.setCacheObject("login:" + userId,loginUser);
return new ResponseResult(200,"登录成功",map);
}
}
认证过滤
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("进入了过滤器");
//1 获取token header的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){//非空判断
//放行,让后面的过滤器执行
filterChain.doFilter(request,response);
return;
}
//2 token存在,解析token
String userId;
try {
//解析token,里面有一个缓存
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException("token不合法");
}
//3 获取userId,redis获取用户信息
LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
if (loginUser == null){
throw new RuntimeException("该前用户未进行登录");
}
//4 封装Authentication
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//5 存入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//放行,让后面的过滤器执行
filterChain.doFilter(request,response);
}
}
权限管理
1,启动类开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
2,在对应使用方法上加上权限注解
@PreAuthorize("hasAuthority('sayhello')")
3,封装返回的UserDetails时把权限也带上,在查询系统用户时,把权限也查上
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private User user;
List<String> permissions;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities!=null){
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
授权表
表模型
CREATE DATABASE /*!32312 IF NOT EXISTS*/`ydl_security` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `ydl_security`;
/*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`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 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`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 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`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*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`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SELECT DISTINCT perms from sys_menu where id in (
SELECT menu_id from sys_role_menu where role_id in (
SELECT role_id from sys_user_role where user_id=1
)
) and status='0'
自定义报错
因为他是springsecruity内部处理的,不方便我们直接抛全局异常,我们可以进行注入两个处理类来处理
- 认证失败
- AuthenticationException去调用AuthenticationEntryPoint的commence方法
- 授权失败
- AccessDeniedException去调用AccessDeniedHandler的handle方法处理
全部总结
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springSer</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springSer</name>
<description>springSer</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
封装user,实现UserDetails来配套接口返回
其中还有一些权限,在实现UserDetailService的loadUser方法中可以实现
@Data
@NoArgsConstructor
@AllArgsConstructor
//这个UserDetails就是就是把系统用户的信息转化为UserDetails供springSeriurty的UserDetailsService去使用的
public class LoginUser implements UserDetails {
private User user;
List<String> permissions;
@JSONField(serialize = false)
List<SimpleGrantedAuthority> authorities;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null){
return authorities;
}
authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
实现UserDetailsService
在UserDetailManager中springSecurity实现了查询系统用户,我们也可以直接实现他的接口来查询
- 用户信息
- 设置权限信息
//实现UserDetailsService 重写loadUserByUsername 就是为了获取系统用户
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
User user = new User();
if (user == null){
throw new RuntimeException("用户不存在");
}
user.setUserId("111");
user.setUsername("hjh");
user.setPassword("{noop}123456");
//查询用户权限信息 到数据库去查询该用户的权限
List<String> list = new ArrayList<>(Arrays.asList("sayhell", "delgoodes"));
return new LoginUser(user,list);
}
}
配置类
配置类挺多东西要弄的
- AuthenticationManager,后面要给service层调用auth方法进行验证
- 设置开发的一些接口,比如登录,注册,
- 注入密码加盐类
- 把token的校验器放入过滤器链中
- 认证过程和鉴权过程的异常处理
- 跨域问题
@Configuration
public class SecurityConfig {
@Autowired
private AuthenticationConfiguration authenticationConfiguration;
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
// @Bean
// public PasswordEncoder passwordEncoder(){
// return new BCryptPasswordEncoder();//这里会自动加盐
//
// }
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/demo/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//告诉security如何处理异常
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允许跨域
http.cors();
return http.build();
}
//把验证的Manager放进去spring中,方便在service中调用auth方法
@Bean
public AuthenticationManager authenticationManager() throws Exception{
AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
return authenticationManager;
}
}
处理认证失败
//处理认证失败的
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//给前端ResponseResult 的json
ResponseResult responseResult = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "登陆认证失败了,请重新登陆!");
String json = JSON.toJSONString(responseResult);
WebUtils.renderString(response,json);
}
}
处理鉴权失败
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
//到时可以结合自己的全局异常,或者一些返回类来进行返回策略
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//给前端ResponseResult 的json
ResponseResult responseResult = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您权限不足!");
String json = JSON.toJSONString(responseResult);
WebUtils.renderString(response,json);
}
}
跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
过滤认证器
- 获取token
- 获取失败就放行
- 解析token,拿到userId
- 去redis取出user
- 封装Authentication,有一些权限信息
- 存入SecurityContextHolder供其他方便使用
Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1 获取token header的token
String token = request.getHeader("token");
if(!StringUtils.hasText(token)){//非空判断
//放行,让后面的过滤器执行
filterChain.doFilter(request,response);
return;
}
//2 token存在,解析token
String userId;
try {
//解析token,里面有一个缓存
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException("token不合法");
}
//3 获取userId,redis获取用户信息
LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
if (loginUser == null){
throw new RuntimeException("该前用户未进行登录");
}
//4 封装Authentication
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//5 存入SecurityContextHolder中
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
//放行,让后面的过滤器执行
filterChain.doFilter(request,response);
}
}
开启鉴权
@EnableGlobalMethodSecurity(prePostEnabled = true)