spring boot项目整合spring security权限认证

一、准备一个spring boot项目

1、引入基础依赖

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.1.13</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>5.1.6</version>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.3.1</version>
		</dependency>
	</dependencies>

2、项目配置文件,连接数据库

server:
  port: 80
  servlet:
    context-path: /

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/security?serverTimezone=Asia/Shanghai
    username: root
    password: zzybzb

3、一个简单接口

package com.pzz.controller;

import com.pzz.mapper.XcUserMapper;
import com.pzz.po.XcUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
public class LoginController {
    
    

    @Autowired
    XcUserMapper userMapper;

    @RequestMapping("/login-success")
    public String loginSuccess() {
    
    

        return "登录成功";
    }

    @RequestMapping("/r/r1")
    public String r1() {
    
    
        return "访问r1资源";
    }

    @RequestMapping("/r/r2")
    public String r2() {
    
    
        return "访问r2资源";
    }
    
}

4、测试接口能跑

在这里插入图片描述

二、整合spring security

1、引入依赖配置

		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-security</artifactId>
			<version>2.1.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-oauth2</artifactId>
			<version>2.1.3.RELEASE</version>
		</dependency>

2、启动,再次访问需要登录

这里的admin无法登录
在这里插入图片描述

3、添加配置文件

  • 用户信息
    在内存配置两个用户:zhangsan、lisi
    zhangsan用户拥有的权限为p1
    lisi用户拥有的权限为p2
  • 密码方式
    暂时采用明文方式
  • 安全拦截机制
    /r/**开头的请求需要认证
package com.pzz.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * @description 安全管理配置
 */
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }
    //配置用户信息服务
    @Bean
    public UserDetailsService userDetailsService() {
    
    
        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //在内存创建zhangsan,密码123,分配权限p1
        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
        //在内存创建lisi,密码456,分配权限p2
        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
        return manager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
//        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
//        return new BCryptPasswordEncoder();
    }

    //配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http
                .authorizeRequests()
                .antMatchers("/r/**").authenticated()//访问/r开始的请求需要认证通过
                .anyRequest().permitAll()//其它请求全部放行
                .and()
                .formLogin().successForwardUrl("/login-success");//登录成功跳转到/login-success

        http.logout().logoutUrl("/logout");//退出地址

    }

}

4、访问测试

  • 只拦截/r/**路径下的请求,所以login-success没问题
    在这里插入图片描述
  • 访问/r/r1需要登录在这里插入图片描述
  • 输入配置文件设置的用户名密码登录
    在这里插入图片描述在这里插入图片描述
  • 再次访问,不需要登录
    在这里插入图片描述

5、测试退出

  • 配置文件配置了退出的路径

在这里插入图片描述

  • 退出登录,并再次请求/r/r1

在这里插入图片描述
在这里插入图片描述

6、授权测试

用户认证通过去访问系统资源时spring security进行授权控制,判断用户是否有该资源的访问权限,如果有则继续访问,如果没有则拒绝访问。

6.1、配置用户拥有哪些权限

张三的权限是p1
李四的权限是p2
在这里插入图片描述

6.2、指定资源与权限的关系

    @RequestMapping("/r/r1")
    @PreAuthorize("hasAuthority('p1')")//拥有p1权限方可访问
    public String r1() {
    
    
        return "访问r1资源";
    }


    @RequestMapping("/r/r2")
    @PreAuthorize("hasAuthority('p2')")//拥有p2权限方可访问
    public String r2() {
    
    
        return "访问r2资源";
    }

6.3、重启,访问测试

  • 张三登录,分别访问/r/r1和/r/r2
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  • lisi登录,分别访问/r/r1和/r/r2
    在这里插入图片描述在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

7、工作原理

7.1、工作原理

pring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。

当初始化Spring Security时,会创建一个名为SpringSecurityFilterChain的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:
在这里插入图片描述

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理。

spring Security功能的实现主要是由一系列过滤器链相互配合完成。

在这里插入图片描述

下面介绍过滤器链中主要的几个过滤器及其作用:

  • SecurityContextPersistenceFilter
    这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。
    在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
  • UsernamePasswordAuthenticationFilter
    用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;
  • FilterSecurityInterceptor
    是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
  • ExceptionTranslationFilter
    能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

7.2、执行流程

Spring Security的执行流程如下:
在这里插入图片描述

  1. 用户提交用户名、密码被SecurityFilterChain中的UsernamePasswordAuthenticationFilter过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证。
  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。
  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。
  5. 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

三、连数据库

1、实体类信息

@Data
@TableName("xc_user")
public class XcUser implements Serializable {
    
    

    private static final long serialVersionUID = 1L;

    private String id;

    private String userName;

    private String password;

    private String salt;

    private String name;
    private String nickname;
    private String wxUnionid;
    private String companyId;
    /**
     * 头像
     */
    private String userpic;

    private String utype;

    private Timestamp birthday;

    private String sex;

    private String email;

    private String cellphone;

    private String qq;

    /**
     * 用户状态
     */
    private String status;

    private Timestamp createTime;

    private Timestamp updateTime;

}

2、数据库表

DROP TABLE IF EXISTS `xc_user`;
CREATE TABLE `xc_user`  (
  `id` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `user_name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `password` varchar(96) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `salt` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `wx_unionid` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '微信unionid',
  `nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
  `name` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `userpic` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
  `company_id` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `utype` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `birthday` datetime NULL DEFAULT NULL,
  `sex` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `email` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `cellphone` varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `qq` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `status` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户状态',
  `create_time` datetime NOT NULL,
  `update_time` datetime NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `unique_user_username`(`user_name`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = DYNAMIC;

3、mapper

@Mapper
public interface XcUserMapper extends BaseMapper<XcUser> {
    
    

}

4、实现UserDetailsService接口,重写loadUserByUsername方法

package com.pzz.service;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.pzz.domain.LoginUser;
import com.pzz.mapper.XcUserMapper;
import com.pzz.po.XcUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

/**
 * 用户验证处理
 *
 * @author ruoyi
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    
    private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);

    @Autowired
    private XcUserMapper xcUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        LambdaQueryWrapper<XcUser> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(XcUser::getUserName, username);
        XcUser xcUsers = xcUserMapper.selectOne(lambdaQueryWrapper);
        //如果沒有用戶就拋出异常
        if (ObjectUtils.isEmpty(xcUsers)) {
    
    
            throw new RuntimeException("用户名或密码错误");
        }
        //查到用户,返回信息
        return new LoginUser(xcUsers);
    }
}

5、封装返回的UserDetails

@Data
public class LoginUser implements UserDetails
{
    
    
    private static final long serialVersionUID = 1L;

    /**
     * 用户ID
     */
    private Long userId;

    /**
     * 部门ID
     */
    private Long deptId;

    /**
     * 用户唯一标识
     */
    private String token;

    /**
     * 登录时间
     */
    private Long loginTime;

    /**
     * 过期时间
     */
    private Long expireTime;

    /**
     * 登录IP地址
     */
    private String ipaddr;

    /**
     * 登录地点
     */
    private String loginLocation;

    /**
     * 浏览器类型
     */
    private String browser;

    /**
     * 操作系统
     */
    private String os;

    /**
     * 权限列表
     */
    private Set<String> permissions;

    /**
     * 用户信息
     */
    private XcUser user;

    public LoginUser()
    {
    
    
    }

    public LoginUser(XcUser user)
    {
    
    
        this.user = user;
    }

    public LoginUser(XcUser user, Set<String> permissions)
    {
    
    
        this.user = user;
        this.permissions = permissions;
    }

    public LoginUser(Long userId, Long deptId, XcUser user, Set<String> permissions)
    {
    
    
        this.userId = userId;
        this.deptId = deptId;
        this.user = user;
        this.permissions = permissions;
    }

    @Override
    public String getPassword()
    {
    
    
        return user.getPassword();
    }

    @Override
    public String getUsername()
    {
    
    
        return user.getUserName();
    }

    /**
     * 账户是否未过期,过期无法验证
     */
    @Override
    public boolean isAccountNonExpired()
    {
    
    
        return true;
    }

    /**
     * 指定用户是否解锁,锁定的用户无法进行身份验证
     * 
     * @return
     */
    @Override
    public boolean isAccountNonLocked()
    {
    
    
        return true;
    }

    /**
     * 指示是否已过期的用户的凭据(密码),过期的凭据防止认证
     * 
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired()
    {
    
    
        return true;
    }

    /**
     * 是否可用 ,禁用的用户不能身份验证
     * 
     * @return
     */
    @Override
    public boolean isEnabled()
    {
    
    
        return true;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities()
    {
    
    
        return null;
    }
}

6、明文登录

6.1、设置明文

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
	    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
        //密码为明文方式
        return NoOpPasswordEncoder.getInstance();
//        return new BCryptPasswordEncoder();
    }
}

6.2、测试

在这里插入图片描述

在这里插入图片描述

7、密码加密登录

7.1、设置密码加密

    @Bean
    public PasswordEncoder passwordEncoder() {
    
    
//        //密码为明文方式
//        return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

7.2、生成密码,存入数据库

在这里插入图片描述在这里插入图片描述

7.3、测试,登录成功

在这里插入图片描述

8、注意,把自定义的用户名密码去掉

在这里插入图片描述

四、整合jwt,登录成功返回用户信息

1、引入依赖

		<!-- redis 缓存操作 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<!-- JWT-->
		<dependency>
			<groupId>io.jsonwebtoken</groupId>
			<artifactId>jjwt</artifactId>
			<version>0.5.1</version>
		</dependency>

2、JwtUtils工具类

package com.pzz.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

public class JwtUtils {
    
    
  
    private static final String SECRET_KEY = "yourSecretKey";
    private static final long EXPIRATION_TIME = 86400000; // 过期时间为1天

    /**
     * 用于生成JWT。
     * 它接收一个用户ID作为参数,并使用内置的算法和秘钥生成一个包含用户ID和过期时间的签名。
     * @param userId
     * @return
     */
    public static String generateToken(String userId) {
    
    
        Date expirationDate = new Date(System.currentTimeMillis() + EXPIRATION_TIME);
        return Jwts.builder()
                .setSubject(userId)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, SECRET_KEY)
                .compact();
    }

    /**
     * 用于从JWT中提取用户ID。
     * 它接收一个JWT令牌作为参数,并解析其中的主题(主体)部分来获取用户ID。
     * @param token
     * @return
     */
    public static String getUserIdFromToken(String token) {
    
    
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject();
    }

    /**
     * 用于验证JWT的有效性。
     * 它接收一个JWT令牌作为参数,并解析和验证签名。
     * 如果令牌有效且未过期,则返回true;否则返回false。
     * @param token
     * @return
     */
    public static boolean isTokenValid(String token) {
    
    
        try {
    
    
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
    
    
            return false;
        }
    }
}

3、redis 工具类

package com.pzz.utils;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * spring redis 工具类
 *
 * @author ruoyi
 **/
@SuppressWarnings(value = {
    
     "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    
    
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     */
    public <T> void setCacheObject(final String key, final T value)
    {
    
    
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     */
    public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
    {
    
    
        redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout)
    {
    
    
        return expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 设置有效时间
     *
     * @param key Redis键
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return true=设置成功;false=设置失败
     */
    public boolean expire(final String key, final long timeout, final TimeUnit unit)
    {
    
    
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 获取有效时间
     *
     * @param key Redis键
     * @return 有效时间
     */
    public long getExpire(final String key)
    {
    
    
        return redisTemplate.getExpire(key);
    }

    /**
     * 判断 key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public Boolean hasKey(String key)
    {
    
    
        return redisTemplate.hasKey(key);
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(final String key)
    {
    
    
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public boolean deleteObject(final String key)
    {
    
    
        return redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection 多个对象
     * @return
     */
    public boolean deleteObject(final Collection collection)
    {
    
    
        return redisTemplate.delete(collection) > 0;
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> long setCacheList(final String key, final List<T> dataList)
    {
    
    
        Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
        return count == null ? 0 : count;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(final String key)
    {
    
    
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet)
    {
    
    
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
    
    
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(final String key)
    {
    
    
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     */
    public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
    {
    
    
        if (dataMap != null) {
    
    
            redisTemplate.opsForHash().putAll(key, dataMap);
        }
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(final String key)
    {
    
    
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 往Hash中存入数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @param value 值
     */
    public <T> void setCacheMapValue(final String key, final String hKey, final T value)
    {
    
    
        redisTemplate.opsForHash().put(key, hKey, value);
    }

    /**
     * 获取Hash中的数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return Hash中的对象
     */
    public <T> T getCacheMapValue(final String key, final String hKey)
    {
    
    
        HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
        return opsForHash.get(key, hKey);
    }

    /**
     * 获取多个Hash中的数据
     *
     * @param key Redis键
     * @param hKeys Hash键集合
     * @return Hash对象集合
     */
    public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
    {
    
    
        return redisTemplate.opsForHash().multiGet(key, hKeys);
    }

    /**
     * 删除Hash中的某条数据
     *
     * @param key Redis键
     * @param hKey Hash键
     * @return 是否成功
     */
    public boolean deleteCacheMapValue(final String key, final String hKey)
    {
    
    
        return redisTemplate.opsForHash().delete(key, hKey) > 0;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(final String pattern)
    {
    
    
        return redisTemplate.keys(pattern);
    }
}

4、redis的配置

spring:
  redis:
    host: 127.0.0.1
    post: 6379

5、创建authenticationManagerBean对象,和配置安全拦截机制

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    

    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
    
    
        return super.authenticationManagerBean();
    }

	//配置安全拦截机制
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        http     //关闭csrf
                .csrf().disable()
                //不通过session获取securityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                //放行登录接口
                .antMatchers("/login").anonymous()
                //除了上面的请求,其他的请求必须授权认证
                .anyRequest().authenticated();
    }
}

6、登录的接口

    /**
     * 登录的方法
     * @return
     */
    @PostMapping("/login")
    public R loginSuccess(@RequestBody XcUser xcUser) {
    
    
        return loginService.login(xcUser);
    }

7、登录的实现,使用用户ID生成jwt

@Service
public class LoginServiceImpl implements LoginService {
    
    

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    /**
     * 登录认证的方法
     * @param xcUser
     * @return
     */
    @Override
    public R login(XcUser xcUser) {
    
    
        //Authentication authenticate 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(xcUser.getUserName(), xcUser.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        //认证失败,返回异常
        if (ObjectUtils.isEmpty(authenticate)){
    
    
            throw  new RuntimeException("登录失败...");
        }
        //认证通过,生成jwt,返回
        LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        //根据用户ID生成jwt
        String jwt = JwtUtils.generateToken(userId);
        Map<String,String> map = new HashMap<>();
        map.put("token",jwt);
        //把完整的用户信息存入redis,userId作为key
        redisCache.setCacheObject("login:"+userId,loginUser);
        String username = loginUser.getUser().getUserName().toString();
        return R.ok(map,"登录成功,欢迎:"+username);
    }
}

8、测试接口

在这里插入图片描述

五、认证过滤器

当用户登录后,生成的token信息存入headers中,每次请求应该携带token,作为是否登录的验证

1、认证过滤器JwtAuthenticationTokenFilter

package com.pzz.filter;

import com.pzz.domain.LoginUser;
import com.pzz.utils.JwtUtils;
import com.pzz.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.rmi.RemoteException;
import java.util.Objects;

/**
 * @author pzz
 * @date 2023/7/26 22:51
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    
    

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    
    
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)){
    
    
            //放行
            filterChain.doFilter(request,response);
            return;
        }
        //解析token
        String userId;
        try {
    
    
            userId = JwtUtils.getUserIdFromToken(token);
        } catch (Exception e) {
    
    
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }

        //从redis中获取用户信息
        String redisKey = "login:"+userId;
        LoginUser loginUser = (LoginUser)redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
    
    
            throw new RuntimeException("用户未登录");
        }

        //存入securContextHolder
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,
                null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
}

2、配置认证过滤器

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
	  @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

	  @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
		//配置认证过滤器
    	http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
	}
}

3、根据ID查询用户信息的方法

    /**
     * 根据用户ID查询用户信息
     * @param id
     * @return
     */
    @RequestMapping("/user/{id}")
    public R getuser(@PathVariable("id") String id) {
    
    
        XcUser xcUser = userMapper.selectById(id);
        return R.ok(xcUser);
    }

4、测试

4.1、未携带token

在这里插入图片描述

4.2、携带token

在这里插入图片描述

5、退出登录

5.1、controller代码

    /**
     * 注销登录
     */
    @GetMapping("/logout")
    public R logout(){
    
    
        return loginService.logout();
    }

5.2、service代码

    /**
     * 退出登录
     * @return
     */
    @Override
    public R logout() {
    
    
        //获取SecurityContextHolder中的用户ID
        UsernamePasswordAuthenticationToken authenticationToken = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser)authenticationToken.getPrincipal();
        String userId = loginUser.getUser().getId();
        //删除redis中的值
        redisCache.deleteObject("login:"+userId);
        return R.ok(null,"注销成功");
    }

六、授权

1、相关配置

1.1、开启访问资源所需权限

@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    }

1.2、访问路径设置权限

    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")//拥有test权限方可访问
    public String hello() {
    
    
        return "hello";
    }

2、封装权限、给用户添加权限

2.1、封装用户的实体类,用户设置权限

@Data
public class LoginUser implements UserDetails{
    
    
	    /**
     * 权限列表
     */
    private Set<String> permissions;

	public LoginUser(XcUser user, Set<String> permissions)
    {
    
    
        this.user = user;
        this.permissions = permissions;
    }

	@JSONField(serialize = false)
    private 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;
    }
}

2.2、用户登录时查询用户的权限,当前设置用户的权限为test

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    
	    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    
    
        //查询用户的权限
        Set<String> set = new HashSet<>();
        set.add("test");
        //查到用户,返回信息
        return new LoginUser(xcUsers,set);
    }
}

2.3、过滤器拦截权限

在这里插入图片描述

3、测试

3.1、登录后,访问hello

在这里插入图片描述

3.2、修改hello的访问权限为test111,测试

在这里插入图片描述
在这里插入图片描述

4、从数据查用户的权限

权限一般使用五个表

  1. 用户表
  2. 角色表
  3. 用户_角色_中间表
  4. 菜单表(权限表)
  5. 角色_权限_中间表

4.1、查询权限的SQL

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.pzz.mapper.MenuMapper">

    <select id="selectPermsByUserId" resultType="java.lang.String" parameterType="integer">
        SELECT
            DISTINCT
            sm.perms
        from
            sys_user_role sur
        left join sys_role sr on sur.role_id = sr.role_id
        LEFT JOIN sys_role_menu srm on sr.role_id = srm.role_id
        left join sys_menu sm on srm.menu_id = sm.menu_id
        where
            sm.perms is not null
            and sm.perms != ""
            and sur.user_id = #{userId}
    </select>
</mapper>

4.2、登录成功时,设置用户的权限

在这里插入图片描述

4.3、修改访问hello接口的权限

在这里插入图片描述

4.4、访问测试

在这里插入图片描述

七、跨域

1、springboot设置跨域(省略)

2、security设置跨域

 //设置跨域
 http.cors();

八、security认证流程

在这里插入图片描述

结束!
hy:23


							真正的幸福来自于内心的满足和积极的生活态度。

猜你喜欢

转载自blog.csdn.net/weixin_49107940/article/details/131258029