spring security 笔记

文章详细介绍了如何使用SpringSecurity进行用户认证,包括实现UserDetails服务,自定义LoginUser实体类以封装用户信息和权限,以及配置JWT认证过滤器。同时,文章提到了权限控制和异常处理,如AccessDeniedHandler和AuthenticationEntryPoint,以及自定义的权限表达式根类。另外,还涉及到MyBatis-Plus的Mapper文件,用于查询用户权限。

1.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

实现服务接口,查询DB用户信息和所有权限,返回到LoginUser封装类中。

package com.example.securitydemo1.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.securitydemo1.entity.LoginUser;
import com.example.securitydemo1.entity.User;
import com.example.securitydemo1.mapp.MenuMapper;
import com.example.securitydemo1.mapp.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;

/**
 * @author 朱大仙
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名错误");
        }
        // 根据用户查询权限信息 添加到LoginUser中 现在只是给了个假
        List<String> perms = menuMapper.selectPermsByUserId(user.getId());

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

2.用户实现类LoginUser

在使用spring security时需要一个用户实现类LoginUser

去实现UserDetails并且带有User 变量。这里有7个继承方法

getAuthorities()      UserDetails返回用户权限

getPassword() {      UserDetails返回用户密码
getUsername() {      UserDetails返回用户名
isAccountNonExpired() {      UserDetails指示用户的帐户是否已过期。无法对过期的帐户进行身份验证。
isAccountNonLocked() {      UserDetails指示用户是已锁定还是未锁定。无法对锁定的用户进行身份验证

isCredentialsNonExpired() {      UserDetails指示用户的凭据(密码)是否已过期。过期的凭据阻止身份验证。
isEnabled() {      UserDetails指示用户是启用还是禁用。无法对禁用的用户进行身份验证。

package com.example.securitydemo1.entity;

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;

/**
 * @author 朱大仙
 */
@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() {
       /* List<SimpleGrantedAuthority> list= new ArrayList<>();
        for (String permission : permissions) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            list.add(simpleGrantedAuthority);
        }*/

        if (authorities != null) {
            return authorities;
        }
        //变式stream
        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;
    }
}

3.security配置类

package com.example.securitydemo1.config;

import com.example.securitydemo1.filter.JwtAuthenticationTokenFilter;
import com.example.securitydemo1.handle.AccessDeniedHandlerImpl;
import com.example.securitydemo1.handle.AuthenticationEntryPointImpl;
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.authentication.configuration.AuthenticationConfiguration;
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.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author 朱大仙
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    AuthenticationEntryPointImpl authenticationEntryPoint;
    @Autowired
    AccessDeniedHandlerImpl accessDeniedHandler;
    /**
     * 密码加密存储 BCryptPasswordEncoder 自动加盐
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 以前我们自定义类继承自 WebSecurityConfigurerAdapter 来配置我们的 Spring Security,我们主要是配置两个东西:
     *
     * configure(HttpSecurity)
     * configure(WebSecurity)
     * 前者主要是配置 Spring Security 中的过滤器链,后者则主要是配置一些路径放行规则。
     *
     * 现在在 WebSecurityConfigurerAdapter 的注释中,人家已经把意思说的很明白了:
     *
     * 以后如果想要配置过滤器链,可以通过自定义 SecurityFilterChainBean来实现。
     * 以后如果想要配置 WebSecurity,可以通过 WebSecurityCustomizerBean来实现。
     */
    @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();

        //添加过滤器,在UsernamePasswordAuthenticationFilter过滤器前
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        //security自定义异常处理
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)
                .accessDeniedHandler(accessDeniedHandler);

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

    @Autowired
    private AuthenticationConfiguration authenticationConfiguration;

    @Bean
    public AuthenticationManager authenticationManager() throws Exception{
        AuthenticationManager authenticationManager = authenticationConfiguration.getAuthenticationManager();
        return authenticationManager;
    }

}

没有实现WebSecurityConfigurerAdapter接口。

@aEnbleGlobalMethodSecurity(prePostEnabled = true) 开启全局注解
注入了authenticationManager的bean,以从获得权限认证方法

其中配置了JwtAuthenticationTokenFilter用户认证过滤器,放在UsernamePasswordAuthenticationFilter过滤器前面。

package com.example.securitydemo1.filter;

import com.alibaba.fastjson.JSON;
import com.example.securitydemo1.entity.LoginUser;
import com.example.securitydemo1.utils.JwtUtil;
import com.example.securitydemo1.utils.RedisCache;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
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.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;

/**
 * @author 朱大仙
 */
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
    @Autowired
    RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//        1.获取token 前端指定存在header的
        String token = request.getHeader("token");
        if (Objects.isNull(token)) {
            //放行 让后面的过滤器执行
            filterChain.doFilter(request,response);
            return;
        }
//        2.解析token
        String userId;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userId = claims.getSubject();
        } catch (Exception e) {
            throw new RuntimeException("token不合法!");
        }

//        3.获取userid
        LoginUser loginUser =redisCache.getCacheObject("login:" + userId);
        log.info("loginUser:{}",loginUser);
        if (Objects.isNull(loginUser)) {
            throw new RuntimeException("当前用户未登入");
        }
//        4.封装Authentication
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());

//        5.存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

        filterChain.doFilter(request,response);
    }
}

JwtAuthenticationTokenFilter 中的token是通过工具类JwtUtil 实现,整个过程中我们的JWT只是在自定义过滤器中判断是否存在,进而判断是否经过登入访问。userId就是放在jwt的载荷当中。

JSON Web Token (JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息.一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象.["typ";"JWT""alg":"HS256"}

在头部指明了签名算法是HS256算法。我们进行BASE64编码http://base64.xpcha.com/,编码后的字符串如下:

eyJ@eXAi0iJKV1QiLCJhbGci0iJIUzI1NiJ9

载荷 (playload)

载荷就是存放有效信息的地方。

定义一个payload:

("sub";"1234567890","name";"itlils""admin";true,"age":18}

然后将其进行base64加密,得到wt的第二部分

签证 (signature)

eyJzdwIi0iIxMjM0NTY30DkwIiwibmFtZSI6Iml0bGlscyIsImFkbwluIjp0cnV1LCJhZ2Ui0jE4fQ=

jwt的第三部分是一个签证信息,

配置了自定义用户认证、权限认证异常类。

package com.example.securitydemo1.handle;

import com.alibaba.fastjson.JSON;
import com.example.securitydemo1.comm.ResponseResult;
import com.example.securitydemo1.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 朱大仙
 * 权限认证失败返回消息
 */
@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 jsonString = JSON.toJSONString(responseResult);
        WebUtils.renderString(response, jsonString);
    }
}
package com.example.securitydemo1.handle;

import com.alibaba.fastjson.JSON;
import com.example.securitydemo1.comm.ResponseResult;
import com.example.securitydemo1.utils.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 朱大仙
 * 身份验证失败返回消息
 */
@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 jsonString = JSON.toJSONString(responseResult);
        WebUtils.renderString(response, jsonString);
    }
}

​ SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。

​ 我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,关闭csrf

跨域配置类

package com.example.securitydemo1.config;

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

/**
 * @author 朱大仙
 * 跨域配置
 */
@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);
    }
}

 自定义配置权限控制代码

package com.example.securitydemo1.comm;

import com.example.securitydemo1.entity.LoginUser;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

/**
 * @author 朱大仙
 * 自定义配置权限控制
 */
@Component("ex")
public class LLSExpressionRoot {

    public boolean hasAuthority(String authority){
        //获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        List<String> permissions = loginUser.getPermissions();
        //判断用户权限集合中是否存在authority
        return permissions.contains(authority);
    }
}

用户通过security的过滤器链后,loginUser会保存在SecurityContextHolder中,再保存到redis中

在使用redis中,配置类的序列化和反序列化使用时需要注意,是否可以经行正确的对象mapper映射,如果错误,就要判断是否为fastjson的依赖版本问题 或者 反系列化的配置类是否有问题。

序列化配置类

package com.example.securitydemo1.config;

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序列化
 * 
 * @author itlils
 */
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);
    }
}
所有自带权限校验方法
@PreAuthorize("hasAuthority('dev:code:pull1')")
@PreAuthorize("hasAnyAuthority('dev:code:pull1','dev:code:pull')") 只要有其中一个就成功
@PreAuthorize("hasRole('dev:code:pull1')") 每个权限字符串追加ROLE_ 这个前缀
@PreAuthorize("hasAnyRole('dev:code:pull1','dev:code:pull')")每个权限字符串追加ROLE_ 这个前缀,只要有其中一个就成功
@PreAuthorize("ex.hasAuthority('dev:code:pull1')") 自定义权限校验方法

还有对mybatis-plus的​ 自定义方法,所以需要创建对应的mapper文件,定义对应的sql语句

MenuMapper.xml
<mapper namespace="com.example.securitydemo1.mapp.MenuMapper"> 是带@mapper的接口
<?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.example.securitydemo1.mapp.MenuMapper">
    <select id="selectPermsByUserId" parameterType="long" resultType="string">
        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=#{userId}
            )
        ) and status='0'
    </select>
</mapper>

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值