08-SpringBoot (八) --- 整合SpringSecurity

​ 相信看过我上一篇文章的同学会发现一个问题,我们在浏览器直接输入localhost:8080/page就能查询到我们的学生列表信息,也就是说,我们的登录成了摆设,我们可以在不认证登录的情况下直接访问服务端信息,这样的逻辑显然是不合理的,也存在较大的安全隐患。所以,我们需要一个安全框架!

声明

文章为观看B站三更老师视频并结合自己的实例情况所写

Spring Security简介

Spring Secrity就是这样的一个安全管理框架,Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,相比于另一个安全框架Shrio,它的功能更加丰富。中大型项目一般都是使用Spring Security,而Shrio更适合小项目。

Spring Security 核心的两个功能就是:

  • **认证:**就是用户名、密码等进行验证,认证成功之后可以访问系统的信息,否则就拒绝访问
  • **授权:**就是对此时的用户权限判断,即根据不同的权限可以访问不同的信息

前后端分离校验流程

  • 前端携带用户名及密码访问登录接口
  • 后端(服务器)拿到前端传来的数据并与数据库中的用户名及密码进行校验
  • 如果校验成功,使用用户名或者用户id生成一个jwt并将他传给前端
  • 前端登录成功后访问其他需求需要在请求头中携带token
  • 后端对传来的token进行解析,获取用户id,并根据用户id获取用户的信息及权限
  • 如果一切验证都通过就将响应信息传给前端

后端代码的编写

引入依赖

<!--  引入SpringSecurity依赖     -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

​ 当我们引入依赖后我们再次尝试去访问之前的接口就会自动跳转到一个Springsecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

<!--  引入SpringSecurity依赖     -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

​ 当我们引入依赖后我们再次尝试去访问之前的接口就会自动跳转到一个Springsecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

通过DEBUG查看Security过滤器:

​ Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。根据自己的需要,可以使用适当的过滤器来保护自己的应用程序,我们可以通过DEBUG的方式查看有哪些过滤器

在启动类上的改动:

image-20230407105521332

因为SpringApplication.run就是一个容器,所以我们就可以看看这个容器里面有什么

image-20230407105959222

从下图可以看到一共有16种过滤器:

image-20230407110014717

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。认证工作主要有它负责。

**ExceptionTranslationFilter:**处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。

**FilterSecurityInterceptor:**负责权限校验的过滤器。

image-20211214151515385

**Authentication接口:**它的实现类,表示当前访问系统的用户。封装了用户相关信息。

**AuthenticationManager接口:**定义了认证Authentication的方法
**userDetailsService接口:**加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
**UserDetails接口:**提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成userDetails对象返回。然后将这些信忘封装到Authentication对象中。

操作流程

登录阶段

  • 自定义登录接口也就是我们的/login

    • 调用ProviderManager的方法进行认证 如果认证通过生成jwt
    • 把用户信息存入redis中
  • 自定义UserDetailsService

    • 在这个实现类中去查询数据库

校验阶段

  • 定义Jwt认证过滤器
    • 获取token,并解析获取其中的userid
    • 从redis中获取用户信息,存入SecurityContextHolder

工具类及相关配置

添加依赖

<!--   引入fastjson     -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.33</version>
</dependency>

<!--jwt依赖-->
<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>

添加Redis相关配置

新建一个工具包utils在其下存放FastJsonRedisSerializer,JwtUtil,RedisCache,WebUtils

四个工具类

FastJsonRedisSerializer
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.type.TypeFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import com.alibaba.fastjson.parser.ParserConfig;
import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 */
public class FastJsonRedisSerializer<T> implements RedisSerializer<T>
{

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public 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);
    }
}
JwtUtil
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

/**
 * JWT工具类
 */
public class JwtUtil {

    //有效期为
    public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000  一个小时
    //设置秘钥明文
    public static final String JWT_KEY = "bphere";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        return token;
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis==null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)              //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("bp")     // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
        return builder.compact();
    }

    public static void main(String[] args) throws Exception {
        String jwt = createJWT("123456");
        // 这就是我们获得的jwt,输出结果就是下面的token, 设置了有效时间的
        System.out.println(jwt);
        // 开始解密
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4MzRiMzA2MjY4Nzg0MjFmOGZhMjE1YWIyZTBlMTRiNSIsInN1YiI6IjEyMzQ1NiIsImlzcyI6InNnIiwiaWF0IjoxNjgwODU0MzY3LCJleHAiOjE2ODA4NTc5Njd9.Vh8nPnQZ00DlvkMsWZVq2xbtJ23BXZ1F2FHIDYKmc10";
        Claims claims = parseJWT(token);
        System.out.println(claims);
        // 输出结果:{jti=834b30626878421f8fa215ab2e0e14b5, sub=123456, iss=bp, iat=1680854367, exp=1680857967}
        System.out.println(claims.getSubject());
        // 输出:123456
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
RedisCache
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;

@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 缓存键值
     * @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 long deleteObject(final Collection collection)
    {
        return redisTemplate.delete(collection);
    }

    /**
     * 缓存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
     * @param hkey
     */
    public void delCacheMapValue(final String key, final String hkey)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.delete(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);
    }

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

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;
    }
}
RedisConfig

在包config包下新增redis配置类RedisConfig

import com.pan.utils.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = { "unchecked", "rawtypes" })
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
    {
        RedisTemplate<Object, 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;
    }
}
通用响应类ResponseResult

可以存放在包common

import com.fasterxml.jackson.annotation.JsonInclude;


@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseResult<T> {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提示信息,如果有错误时,前端可以获取该字段进行提示
     */
    private String msg;
    /**
     * 查询到的结果数据,
     */
    private T data;

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(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 ResponseResult(Integer code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }
}
实体类User

依旧使用我们的老数据user实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data               // 返回set,get等方法
@NoArgsConstructor  // 无参构造器
@AllArgsConstructor // 全参构造器
public class User {
    private Long id;
    private String name;
    private String pwd;

}

认证实现

​ 我们可以自定义一个UserDetailsService,让SpringSecurity使用我们的UserDetailsService。我们自己的UserDetailsService可以从数据库中查询用户名和密码

​ 创建一个类实现UserDetailsService接口,重写其中的方法。更加用户名从数据库中查询用户信息

实现UserDetailsService接口
UserDetailsServiceImpl
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.pan.entity.LoginUser;
import com.pan.entity.User;
import com.pan.mapper.UserMapper;
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 java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;


@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        // 根据用户查询权限信息 添加到LoginUser中
        List<String> list = new ArrayList<>(Arrays.asList("test","admin"));

        //封装成UserDetails对象返回
        return new LoginUser(user,list);
    }
}

​ 因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

LoginUser
import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

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

    //存储SpringSecurity所需要的权限信息的集合
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;


    @Override
    public  Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null){
            return authorities;
        }
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}
密码加密存储

​ 实际项目中我们不会把密码明文存储在数据库中,所以我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。

​ 我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。

SecurityConfig
import com.pan.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    @Bean
    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();
    }

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @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();

        //把token校验过滤器添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

//        //配置异常处理器
//        http.exceptionHandling()
//                //配置认证失败处理器
//                .authenticationEntryPoint(authenticationEntryPoint)
//                .accessDeniedHandler(accessDeniedHandler);

        //允许跨域
        http.cors();
    }

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

在我们的SecurityConfig中我们有观察到

@Bean
    public PasswordEncoder passwordEncoder(){

        return new BCryptPasswordEncoder();
    }

我们在测试类里面测试一下它生成的加密密码

@Test
    public void TestBCryptPasswordEncoder(){

        // 或者注入
        /**
         * @Autowired
         * private PasswordEncoder passwordEncoder;
         */
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        String encode =  passwordEncoder.encode("123456");
        String encode2 =  passwordEncoder.encode("654321");
        String encode3 =  passwordEncoder.encode("456789");

        // 值得一提的是每次生成的密码都不一样,这是因为跟盐有关
        System.out.println(encode);
        // $2a$10$yaVBZZ.aKEeadPWJawkk6uJhUTZUpl0n9ndlKVv891m5NFIak2f8m
        System.out.println(encode2);
        // $2a$10$6tMg/Ojk5sd.FjukMFXqsOqI.8gGGfWQR7b.AAN8HF26PiJdkSLMe
        System.out.println(encode3);
        // $2a$10$OkKnELtpfLIfgP0TVUKLb.QN6Lbj61j84Top1UWs4IWMulp.g7QQW

        // 验证结果为 true
        System.out.println(passwordEncoder.matches("123456",
                "$2a$10$yaVBZZ.aKEeadPWJawkk6uJhUTZUpl0n9ndlKVv891m5NFIak2f8m"));

    }

我们将数据库的密码也替换一下

image-20230409211453538

实现登录接口

​ 我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。

​ 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。(上面已经贴出)

​ 认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis,可以把用户id作为key。

LoginServcie
public interface LoginService {
    ResponseResult login(User user);

    ResponseResult logout();
}
LoginController
@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    // controller层不应该写太多的业务逻辑,应该交给service层
    @PostMapping("/login")
    public ResponseResult login(@RequestBody User user){
        return loginService.login(user);
    }

    @RequestMapping("/logout")
    public ResponseResult logout(){
        return loginService.logout();
    }

}
LoginServiceImpl
import com.pan.common.ResponseResult;
import com.pan.entity.LoginUser;
import com.pan.entity.User;
import com.pan.service.LoginService;
import com.pan.utils.JwtUtil;
import com.pan.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Objects;

@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;

    @Override
    public ResponseResult login(User user) {

        // Authentication authenticate 进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(),user.getPwd());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 认证未通过,给出提示
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        //使用userid生成token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = String.valueOf(loginUser.getUser().getId());
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //把token响应给前端
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);

        return new ResponseResult(200,"登陆成功",map);
    }

    @Override
    public ResponseResult logout() {

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"退出成功");
    }
}
认证过滤器

​ 我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder

JwtAuthenticationTokenFilter
import com.pan.entity.LoginUser;
import com.pan.utils.JwtUtil;
import com.pan.utils.RedisCache;
import io.jsonwebtoken.Claims;
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.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.util.Objects;

@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 {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);

        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        // 存入SecurityContextHolder
        // 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);

    }
}

授权实现

​ SpringSecurity为我们提供了基于注解的权限控制方案,我们可以使用注解去指定访问对应的资源所需的权限。

​ 但是要使用它我们需要先开启相关配置,在SecurityConfig类上方添加

@EnableGlobalMethodSecurity(prePostEnabled = true)

然后在使用相应的注解:@PreAuthorize

@RestController
public class StudentController {

    @Autowired
    private StudentMapper studentMapper;

    @RequestMapping("/select/{page}/{size}")
    @PreAuthorize("hasAuthority('test')")	// 注意这里!!!
    public ResponseResult select(@PathVariable("page") int page,@PathVariable("size") int size){

        Page<Student> pages = new Page<>(page, size);
        Page<Student> studentPage = studentMapper.selectPage(pages, null);
        return new ResponseResult(200,studentPage);
    }
}

解决跨域问题

​ 浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

​ 前后端分离项目,前端项目和后端项目一般都不是同源的,我们的前端接口是8080,后端接口是8081,因此会存在跨域问题

添加配置类CorsConfig:

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CrosConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowCredentials(true)
                .allowedHeaders(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .maxAge(3600);
    }
}

到此,后端的配置算是写完了,但是我们之前直接把权限信息写死封装到UserDetails中进行测试。我们写死权限进行测试,但是其实我们可以从数据库中查询权限信息。这里就不再赘述

前端代码的编写

添加路由守卫

​ 在routerindex.js下面添加

router.beforeEach((to, from, next) => {

  if (to.path == '/login') {
      next();
  } else {
      const token = localStorage.getItem('token'); // 获取token
      // token不存在
      if (token === null || token === '') {
          alert('您还没有登录,请先登录');
          next('/login');
      } else {
          next();
      }
  }
});

存入token

store包下的index.js下存入token

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token:localStorage.getItem('token') ? localStorage.getItem('token') : ''
  },
  getters: {
  },
  mutations: {
    // 修改token, 并将token存入localStorage
    changeLogin(state,user){
      state.token = user.token;
      localStorage.setItem('token',user.token);
    }
  },
  actions: {
  },
  modules: {
  }
})

修改LoginView页面

修改LoginView页面,当后端返回状态码是200时,将将用户token保存vuex中

<script>

import Cookies from "core-js/internals/task";
import { mapMutations } from "vuex";

export default {
  name: 'LoginView',
  data () {
    return {
      loginForm: {
        username: '',
        password: ''
      },
      responseResult: []
    }
  },
  methods: {
    ...mapMutations(["changeLogin"]),
    login () {
      const _this = this
      // console.log(_this.loginForm.username)
      // console.log(_this.loginForm.password)
      axios
          .post('/login', {
            "name": _this.loginForm.username, // 注意,你后端的User实体类叫name,pwd
            "pwd": _this.loginForm.password   // 这里也就要叫name!pwd!不然后端接口得不到参数!
          })
          .then(successResponse => {
            // console.log(successResponse.data)
            // console.log(successResponse.data.data.token)
            if (successResponse.data.code === 200) {
              // _this.userToken = "Bearer" + successResponse.data.data.token;
              _this.userToken = successResponse.data.data.token;

              // 将用户token保存vuex中
              _this.changeLogin({token:_this.userToken});
              // 登录成功后将token存储在cookie
              // Cookies.set("Token",successResponse.data.data.token);
              
              _this.$router.push("/page");
              _this.$message('登录成功');
            }else {
              _this.$message('登录失败');
            }
          });
    }
  }
}
</script>

main.js的修改

最后就是在main.js中添加配置,让每次请求的请求头上都带着token信息

import Vue from 'vue'
import './plugins/axios'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import './styles.scss'

Vue.use(ElementUI)

Vue.config.productionTip = false

// 配置后端端口的访问地址
axios.defaults.baseURL = 'http://localhost:8081';


//设置axios请求头加入token
axios.interceptors.request.use(config => {  
   if (localStorage.getItem('token')) {
      config.headers.token = localStorage.getItem('token');
   }
   return config;
},
error => {
   return Promise.reject(error);

});

//=============================
//响应回来token是否过期
axios.interceptors.response.use(response => {  
      console.log('响应回来:'+response.data.code)  
        //和后端token失效返回码约定403    
        if (response.data.code == 403) {
                // 引用elementui message提示框       
                ElementUI.Message({        
                    message: '身份已失效',  
                    type: 'err'        
                    });
                //清除token  
                localStorage.removeItem('token');
                //跳转      
                router.push({name: '/login'});    
            } else { 
                    return response  
            }  
       }, 
  error => { 
     return Promise.reject(error);  
     })

     
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

**注意:**在我们的数据展示页面最好是用console.log(响应)打印一下数据真实存储位置,这样我们的tableData(展示的数据)才能跟着改变!

结果展示

​ 现在,当我们的直接在浏览器输入localhost:8080/page,会被直接拦截,并要求我们登录(原理是我们的请求token为空或者过期),当我们登录成功后,后端验证我们的token,并成功返回数据

image-20230409215951752

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值