SpringSecurity

强烈推荐大家先看视频

  https://www.bilibili.com/video/BV1mm4y1X7Hc/

 

 

 

SpringSecurity初探

想要简单使用一下SpringSecurity,其实很简单,只需要在pom文件中添加如下依赖:

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

然后启动SpringBoot就行了;因为SpringSecurity内置了默认实现。

注:默认用户名为user,密码已经在后台打印了(如果清空了后台,则需重新启动一下)

 

 

原理粗讲

首先我们要记住,SpringSecurity的作用是认证和授权;

何为认证?认证就是去判断你这个用户到底属不属于我这个系统,即我们平常所说的去数据库查看有没有这个用户;

何为授权?授权是认证后的一步,只有认证通过了,才会去进行授权操作;通俗来讲就是规范该用户哪些操作可以进行,哪些操作不能进行;(需要注意的是:此处说讲的授权不是授予用户某个权限,而是验证用户是否具有某个权限

 

登陆校验流程图

即:初次登入会将传入的用户名密码与我方系统中存储在数据库中的数据进行比对,若在数据库中查到该用户信息,则会返回一个jwt(可以简单理解为加密用户标识)给用户,使得用户后续的每次请求只需要传给服务器该jwt,而不再需要输入用户名密码;

 

前面说了,SpringSecurity其实内部已经完整的默认实现;但像SpringSecurity默认提供的首页一般我们是不会使用的,我们会使用自己的首页登入;又例如SpringSecurity默认是去内存校验相应的用户名密码,而实际上我们都是去自己的数据库中进行校验。

 

所以对于SpringSecurity的部分默认实现,我们需要去重载;

那现在的问题就是:哪些方法我们需要重载呢?让我们继续往下看;

 

SpringSecurity的原理本质就是一个过滤器链,简化图如下:

在众多过滤器当中,我们重点需要关注以下三个过滤器

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写的用户名密码后的登陆请求;

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

FilterSecurityInterceptor:负责权限校验的过滤器;去SecurityContextHolder取相应内容。

 

现在我们把目光只放在UsernamePasswordAuthenticationFilter这个过滤器下一定不要搞混局部与全局的关系

  看完上面两张图再结合前面说到的SpringSecurity不足之处,现在我们就需要去实现以下几个方面:

    1、自定义登录接口;但需要注意的是在该接口中本质上还是使用SpringSecurity内置的authenticate方法(因为我们只是修改SpringSecurity的部分功能)

    2、自定义UserDetailsServiceImpl,该类继承至UserDetailsService,来达到在数据库中查询相应数据的效果;

    3、密码加密方式的修改;

    4、若请求携带了正确的jwt,则应该直接放行而无需再登入,所以我们需要在SpringSecurity原本的UsernamePasswordAuthenticationFilter之前加一个自定义过滤器来处理该逻辑;

    5、TODO

 

代码之路

基于MySQL数据库查询用户信息

查看代码
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jxsr.entity.LoginUser;
import com.jxsr.entity.User;
import com.jxsr.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> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(lambdaQueryWrapper);
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        //TODO 根据用户查询权限信息 添加到LoginUser中
        List<String> list = new ArrayList<>(Arrays.asList("test"));
        return new LoginUser(user, list);
    }
}

结合上图来看,该代码中最重要的部分就是loadUserByUsername这个函数;

至于如何返回UserDetails对象,我们创建了一个LoginUser类来继承UserDetails;(可以达到封装自定义属性的效果)

查看代码
 import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
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
@AllArgsConstructor
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<GrantedAuthority> 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.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;
    }
}
查看代码
 import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("user")
public class User {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String userName;
    private String password;
}

 

自定义密码校对方式

@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入AuthenticationManager
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 修改密码加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用CSRF
        http.csrf().disable()
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // 相关路径可匿名访问
                .antMatchers("/test/login", "/test/register").anonymous()
                // 其余路径需要认证
                .anyRequest().authenticated();
    }
}

因此,数据库中的密码需要重新加密生成;(至于登录时的密码校验动作,springsecurity内部已经默认实现了,只要我们修改相应加密方式即可)

关于BCryptPasswordEncoder的相关知识可参考https://www.cnblogs.com/ReturnOfTheKing/p/17271746.html

 

自定义用户登入页面(本质就是重写UsernamePasswordAuthenticationFilter)

controller
 import com.alibaba.fastjson.JSONObject;
import com.jxsr.entity.User;
import com.jxsr.service.IndexService;
import com.jxsr.util.HttpUtil;
import com.jxsr.util.ResultData;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;


@Slf4j
@RestController
@RequestMapping("/index")
public class IndexController {

    @Autowired
    private IndexService indexService;

    @PostMapping("/login")
    public ResultData login(User user){
        return indexService.login(user);
    }
    
    @GetMapping("/logout")
    public ResultData logout(){
        return indexService.logout();
    }
    
}
service
 import com.jxsr.entity.User;
import com.jxsr.util.ResultData;

public interface IndexService {
    ResultData login(User user);
    ResultData logout();
}
serviceImpl
 import com.jxsr.entity.LoginUser;
import com.jxsr.entity.User;
import com.jxsr.service.IndexService;
import com.jxsr.util.JwtUtil;
import com.jxsr.util.RedisCache;
import com.jxsr.util.ResultData;
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 IndexServiceImpl implements IndexService {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisCache redisCache;

    @Override
    public ResultData login(User user) {
        // 用户名密码校验
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);

        if (Objects.isNull(authenticate)){
//            throw new RuntimeException("用户名和密码错误");
            return ResultData.fail("用户名和密码错误");
        }

        // 使用userId生成token
        LoginUser loginUser = (LoginUser)authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);

        //
        redisCache.setCacheObject("login:" + userId, loginUser);
        redisCache.expire("login:" + userId, 60);


        // 把token响应给前端
        HashMap<String, String> objectObjectHashMap = new HashMap<>();
        objectObjectHashMap.put("token", jwt);

        return ResultData.success(objectObjectHashMap);

    }

    @Override
    public ResultData logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Integer userid = loginUser.getUser().getId();
        redisCache.deleteObject("login:"+userid);
        return new ResultData(200,"退出成功", null);
    }
}

 

前面说了,我们本质上还是通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器;

需要强调的是:该过滤器链是层层调用;既然是调用函数,则会有函数返回,所以这个过滤器链其实是一个环路。

 

  前面说过,UsernamePasswordAuthenticationFilter是负责权限校验,如何完成这个校验?

  SecurityContextHolder的作用:保留系统当前的安全上下文细节,其中就包括当前使用系统的用户的信息

  用户第一次登入成功后,服务器会将jwt返回给用户;所以用户后续携带jwt进行访问时,SpringSecurity框架会将redis中存储的完整用户信息存入SecurityContextHolder中;

  需要注意的是每一个请求对应的SecurityContextHolder是不同的(每一次请求都会存储一次)

Jwt过滤器

查看代码
 import com.jxsr.entity.LoginUser;
import com.jxsr.util.JwtUtil;
import com.jxsr.util.RedisCache;
import io.jsonwebtoken.Claims;
import lombok.SneakyThrows;
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;


    @SneakyThrows
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头里的token
        String token = request.getHeader("token");

        if (!StringUtils.hasText(token)) {
            // 放行,因为SpringSecurity内置的其他过滤器会拦截
            filterChain.doFilter(request, response);
            // 防止函数层层调用后返回
            return;
        }
        // 解析token
        Claims claims = JwtUtil.parseJWT(token);
        String userid = claims.getSubject();

        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("用户未登入");
        }
        // 存入SecurityContextHolder
        // 注意三参和两参的区别(三参内部将授权状态设置为了true)
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(request, response);
    }
}

  此处需要特别注意的就是UsernamePasswordAuthenticationToken的构造函数参数个数;三个参数的构造函数会内置一个已授权的标志位;

  且记得将该过滤器加入到SpringSecurity框架中;

// SecurityConfig配置类
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

 

最后,还需要注意如下SpringSecurity配置

@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 注入AuthenticationManager
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 修改密码加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 禁用CSRF
        http.csrf().disable()
                // 不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                .authorizeRequests()
                // 相关路径可匿名访问
                .antMatchers("/test/login", "/test/register").anonymous()
                // 其余路径需要认证
                .anyRequest().authenticated();
    }
}

  

开启注解方式校验权限

1、在SecurityConfig类上添加EnableGlobalMethodSecurity注解

2、在目标方法上添加所需校验的权限

 

 

redis自定义序列化方式

注:此处千万不要使用alibaba提供的序列化方法,否则反序列化会出错

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;



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);
    }
}
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;



@Component
public class RedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        // 使用自定义序列化方式
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);

        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(fastJsonRedisSerializer);
        template.setHashValueSerializer(fastJsonRedisSerializer);

        return template;
    }
}

  

 

RBAC模型(Role-Based Access Control)

本质:给每一个用户一个一个的分配权限,当用户和权限分类过多时,该操作会过于繁琐;因此提出给用户分配角色,给角色分配权限;

 

 

JWT相关概念

Jwt的本质是对用户信息进行加密,类似通行证、信物之类的作用;其全称为JSON Web Token, 通过数字签名的方式,以JSON对象为载体进行数据传输。

三大组成部分:(其中Payload存放的是需要加密的信息,Signature表示签名方式)

代码实现

// 添加依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

生成JWT

    public String createJwt(String userName, String passWd){

        Map<String, Object> map = new HashMap<>();
        map.put("userName", userName);
        map.put("passWd", passWd);

        return Jwts.builder()
                // 加密内容
                .setClaims(map)
                // 加密方式及签名
                .signWith(SignatureAlgorithm.HS256, signature)
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
    }

解析JWT

    public Claims parserJwt(String token){
        return Jwts.parser().setSigningKey(signature).parseClaimsJws(token).getBody();
    }

完整工具类Demo

package com.example.demo.utils;

import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description : jwt工具类
 * @Author : Bruce Lee
 * @CreateTime : 2023/11/29
 */


@Component
public class JwtUtil {
    @Value("${jwt.secret}")
    private String signature;

    @Value("${jwt.expire}")
    private Long expire;


    /**
     * 生成jwt
     * @param userName
     * @param passWd
     * @return
     */
    public String createJwt(String userName, String passWd){

        Map<String, Object> map = new HashMap<>();
        map.put("userName", userName);
        map.put("passWd", passWd);

        return Jwts.builder()
                // 加密内容
                .setClaims(map)
                // 加密方式及签名
                .signWith(SignatureAlgorithm.HS256, signature)
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + expire))
                .compact();
    }

    /**
     * 解析jwt
     * @param token
     * @return
     */
    public Claims parserJwt(String token){
        return Jwts.parser().setSigningKey(signature).parseClaimsJws(token).getBody();
    }

}

 

 

 

参考链接

【1】SpringBootSecurity默认用户名和密码

【2】https://blog.csdn.net/qq_45847507/article/details/126681110

【3】配置类相关函数介绍

【4】UsernamePasswordAuthenticationToken函数介绍

posted @ 2023-03-29 10:50  先娶国王后取经  阅读(45)  评论(0编辑  收藏  举报