SpringSecurity

SpringSecurity简介

ref:

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

Spring Security,这是一种基于 Spring AOPServlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。本教程对 Spring Security 的使用进行一个比较全面的简要介绍。(摘自w3cschool)

创建工程与引入依赖

创建springboot项目或空maven项目

parent

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

dependencys

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</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>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
@RestController
@RequestMapping("/hello")
public class HelloController {
    @GetMapping
    public String hello(){
        return "hello , spring security!";
    }
}

启动项目访问/hello,会跳转到/login,这是默认的

流程

登录校验流程

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
  • FilterSecurityInterceptor:负责权限校验的过滤器。

登录拦截思路

登录

①自定义登录接口

	调用ProviderManager的方法进行认证如果认证通过生成jwt

	把用户信息存入redis中

②自定义UserDetailsService

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

校验:

①定义Jwt认证过滤器

	获取token

	解析token获取其中的userid

	从redis中获取用户信息

	存入SecurityContextHolder

引入redis

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

工具类与配置类

@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisCacheAutoConfiguration {

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用fastJson
        template.setValueSerializer(fastJsonRedisSerializer);
        // hash的value序列化方式采用fastJson
        template.setHashValueSerializer(fastJsonRedisSerializer);

        template.afterPropertiesSet();
        return template;
    }
}
public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;

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

    private final Class<T> clazz;

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

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

    /**
     * 反序列化
     */
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (null == bytes || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T) JSON.parseObject(str, clazz);
    }
}
public class JwtUtil {

    // 默认 生成之后有效时长 1小时
    private static final Long EXPIRE= 60 * 60 * 1000L;
    // 密钥明文
    private static final String JWT_KEY= "securityKey";// 不能有_

    public static String getUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "");
    }


    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @return String
     */
    public static String createJwt(String subject){
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }

    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @param expire 有效时间
     * @return String
     */
    public static String createJwt(String subject, Long expire){
        JwtBuilder builder = getJwtBuilder(subject, expire, getUUID());
        return builder.compact();
    }

    /**
     * 生成jwt
     * @param subject token存放的数据  json格式
     * @param expire 有效时间
     * @param id id
     * @return String
     */
    public static String createJwt(String subject, Long expire, String id){
        JwtBuilder builder = getJwtBuilder(subject, expire, id);
        return builder.compact();
    }


    /**
     *
     * @param subject subject
     * @param expire 过期时长 null 时 默认使用EXPIRATION,1小时
     * @param uuid uuid
     * @return JwtBuilder
     */
    private static JwtBuilder getJwtBuilder(String subject, Long expire, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if (expire == null){
            expire = JwtUtil.EXPIRE;
        }
        long expMillis = nowMillis + expire;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)  // 唯一id
                .setSubject(subject)  // 主题,可以是json
                .setIssuer("ctp")  // 签发者
                .setIssuedAt(now)  // 签发时间
                .signWith(signatureAlgorithm, secretKey)  // 使用HS256算法签名,第二个时密钥
                .setExpiration(expDate);  // 过期时间
    }

    /**
     * 生成加密密钥
     * @return SecretKey
     */
    public static SecretKey generalKey(){
        byte[] decodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKeySpec key = new SecretKeySpec(decodeKey, 0, decodeKey.length, "AES");
        return key;
    }

    /**
     * 解析jwt
     * @param jwt jwt
     * @return Claims
     */
    public static Claims parseJwt(String jwt){
        SecretKey secretKey = generalKey();
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
        return claims;
    }
}
@Component
public class RedisUtil {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

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

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

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

    /**
     * 删除单个对象
     *
     * @param key key
     */
    public void deleteObject(Object key) {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection collection
     */
    public void deleteObject(Collection<Object> collection) {
        redisTemplate.delete(collection);
    }

    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }

    public void expire(String key, int expire, TimeUnit timeUnit) {
        redisTemplate.expire(key, expire, timeUnit);
    }

    /**
     * 缓存List数据
     *
     * @param key      缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public ListOperations<Object, Object> setCacheList(Object key, List<Object> dataList) {
        ListOperations<Object, Object> listOperations = redisTemplate.opsForList();
        if (null != dataList) {
            int size = dataList.size();
            for (Object o : dataList) {
                listOperations.leftPush(key, o);
            }
        }
        return listOperations;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public List<Object> getCacheList(String key) {
        List<Object> dataList = new ArrayList<>();
        ListOperations<Object, Object> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);
        if (null != size) {
            for (int i = 0; i < size; i++) {
                dataList.add(listOperation.index(key, i));
            }
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key     缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public BoundSetOperations<Object, Object> setCacheSet(String key, Set<Object> dataSet) {
        BoundSetOperations<Object, Object> setOperation = redisTemplate.boundSetOps(key);
        for (Object o : dataSet) {
            setOperation.add(o);
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key key
     * @return return
     */
    public Set<Object> getCacheSet(Object key) {
        Set<Object> dataSet = new HashSet<>();
        BoundSetOperations<Object, Object> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key key
     * @param dataMap dataMap
     * @return return
     */
    public HashOperations<Object, Object, Object> setCacheMap(Object key, Map<Object, Object> dataMap) {
        HashOperations<Object, Object, Object> hashOperations = redisTemplate.opsForHash();
        if (null != dataMap) {
            for (Map.Entry<Object, Object> entry : dataMap.entrySet()) {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

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

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

    /**
     * 将字符串渲染到客户端
     * @param resp resp
     * @param str str
     * @return return
     */
    public static String renderString(HttpServletResponse resp, String str){
        try{
            resp.setStatus(200);
            resp.setContentType("application/json");
            resp.setCharacterEncoding("utf-8");
            resp.getWriter().write(str);
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Ret<T> {

    private Integer code;
    private String msg;
    private T data;

    public static Ret ok(){
        return new Ret(200,"ok",null);
    }

    public static Ret fail(){
        return new Ret(200,"ok",null);
    }

    public Ret data(T data){
        this.data = data;
        return this;
    }
}

引入mybatis-plus

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: "0101"
    url: jdbc:mysql://localhost:3306/db_learn?charsetEncoding=utf8&serverTimeZone=UTC

数据库表与实体类

CREATE TABLE `sec_user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  `role` tinyint DEFAULT NULL,
  `status` tinyint DEFAULT NULL,
  PRIMARY KEY (`id`)
) 
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("sec_user")
public class User implements Serializable {
    private static final long serialVersionUID = -4445259547431031783L;
    @TableId
    private Long id;
    private String name;
    private String password;
    private Integer role;
    private Integer status;
}

测试mybatisplus

@Test
public void testMybatisPlus(){
    List<User> users = userMapper.selectList(null);
    System.out.println(users);
}

校验用户

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getName, username);
        User user = userMapper.selectOne(queryWrapper);
        if (Objects.isNull(user)){
            throw new RuntimeException("用户名或者密码错误");
        }

        // TODO 查询用户权限信息

        // 把数据封装成UserDetail并返回
        return new LoginUser(user);
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private static final long serialVersionUID = -3470371303923656730L;
    private User user;
    /**
     *
     * @return 权限信息
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

    /**
     *
     * @return true -> 登录状态未过期
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     *
     * @return true -> 帐户未锁定
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     *
     * @return true -> 签发的凭据未过期
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     *
     * @return true -> 该用户是正常的
     */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如

密码问题PasswordEncoder

实际项目中我们不会把密码明文存储在数据库中。

默认使用的PasswordEncoder要求数据库中的密码格式为: {id}password。 它会根据id去判断密码的加密方式。但是我们一般不会采这种方式。所以就需要替换

PasswordEncoder.

我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。

我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就 会使用该PasswordEncoder来进行密码校验。

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

关于PasswordEncoder,每次加密的盐值不一样,相同的密码最后生成的密文不一样

加密

PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode1 = passwordEncoder.encode("123456");
String encode2 = passwordEncoder.encode("123456");
System.out.println(encode1);
System.out.println(encode2);

输出

$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK
$2a$10$4.XsTRDY.U0AEPESe8ZZxu4VoCv3UnrrAejnQp.xC6EcDO6jmCQEu

比对

boolean b1 = passwordEncoder
        .matches("123456", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
boolean b2 = passwordEncoder
        .matches("1234", "$2a$10$mivDryCWTsusAnEoqslzEO1Ucl4Cu/2yOfxPP0Q6BMVLpciOCcYlK");
System.out.println(b1);// true
System.out.println(b2);// false

安装Redis for windows

安装 redis for windows

https://blog.csdn.net/qq_52385631/article/details/122771598

使用教程

https://blog.csdn.net/weixin_61594803/article/details/122695446

大概步骤

1.下载 redis for windows

2.安装,安装,一路next

(密码设置 安装目录下 redis.windows-service.conf 找到 requirepass ,并设置密码,格式 requirepass xxx)

3.启动redis服务,此电脑->管理->服务

4.安装目录下启动redis.cli,界面显示主机名端口

登录接口

自定义登登录接口,然后让SpringSecurity对这 个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的

authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。

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

key

SecurityConfig重写两个方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // 关闭csrf
        .csrf().disable()
        // 不通过Session获取SecurityContext
        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and()
        .authorizeRequests()
        // 对于登录接口,允许匿名访问
        .antMatchers("/user/login").anonymous()
        // 出上面以外,其他全要验证
        .anyRequest().authenticated();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}
@RestController
public class LoginController {
    @Resource
    private LoginService loginService;
    @PostMapping("/user/login")
    public Ret login(@RequestBody User user){
        return loginService.login(user);
    }
}
@Service
public class LoginServiceImpl implements LoginService {
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private RedisUtil redisUtil;
    @Override
    public Ret login(User user) {
        // AuthenticationManager authenticate进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getName(), user.getPassword());
        Authentication authentication = authenticationManager.authenticate(authenticationToken);// 调用UserDetailServiceImpl.loadUserByUsername认证
        // 未通过则给相应的提示
        if (Objects.isNull(authentication)) {
            throw new RuntimeException("登录失败");
        }
        // 认证通过,使用用户的id生成一个jwt
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long id = loginUser.getUser().getId();
        String jwt = JwtUtil.createJwt(id.toString());
        // 完整信息存入redis,用户的id作为key
        redisUtil.setCacheObject("login:" + id, loginUser);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        return Ret.ok().data(map);
    }
}

访问 http://localhost:8080/user/login

前提需要redis服务,否则失败

{
    "code": 200,
    "msg": "ok",
    "data": {
        "token": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns"
    }
}

检验token

Claims claims = JwtUtil.parseJwt("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxOTRhMzlmMTdlOGI0OTQyODcyZjAyODc2OGJlMDBmYiIsInN1YiI6IjEiLCJpc3MiOiJjdHAiLCJpYXQiOjE2NzA1OTUxOTIsImV4cCI6MTY3MDU5ODc5Mn0.5-m6NmJJRy4DpSbnxY1T-BFEE7vi4P0RB7-kVUD80Ns");
String subject = claims.getSubject();
System.out.println(subject);// 1 (iuserId)

上面代码可用获取到用户id

token认证过滤器

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisUtil redisUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

        // 获取token
        String token = httpServletRequest.getHeader("token");
        if (StringUtils.isEmpty(token)){
            filterChain.doFilter(httpServletRequest, httpServletResponse);
            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 = (LoginUser)redisUtil.getCacheObject(redisKey);
        if (Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        // 存入SecurityHolder
        // TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser, null, null);// authorities表示授权信息
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        // 放行
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

添加到配置SecurityConfig.configure

@Resource
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

退出登录接口

LoginServiceImpl

public Ret logout() {
    // 获取SecurityContextHolder中的用户信息
    UsernamePasswordAuthenticationToken authenticationToken =
            (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authenticationToken.getPrincipal();
    Long userId = loginUser.getUser().getId();
    // 删除redis中对应用户的信息
    redisUtil.deleteObject("login:" + userId);
    return Ret.ok().data("退出成功");
}

SecurityConfig

HttpSecurity.antMatchers("/user/login").anonymous()// 未登录可访问,登录不可访问(携带token,500错误)
HttpSecurity.antMatchers("/user/login").permitAll()// 不管是否登录都可以访问
HttpSecurity.anyRequest().authenticated(); // 登陆了就可以访问

授权引入

基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。 在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的

Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入

Authentication。然后设置我们的资源所需要的权限即可。

模拟授权

UserDetailServiceImpl.loadUserByUsername

...
// TODO 查询用户权限信息
List<String> list= new ArrayList<>(Arrays.asList("login","admin"));
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);

JwtAuthenticationTokenFilter.doFilterInternal

// TODO 获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());// authorities表示授权信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);

配置SecurityConfig

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

LoginController权限注解,下面表示有hello权限才能访问接口

@PostMapping("/hello")
@PreAuthorize("hasAnyAuthority('hello')")
public Ret hello(){
   ...
}

数据库权限与接口访问

数据库表

sec_user

role

user_role

permission

role_permission

根据用户id查询权限sql语句示例

select distinct `p`.`name`
from `user_role` ur
	left join `role_permission` rp on `ur`.role_id = `rp`.role_id
	left join `permission` p on `p`.id =  `rp`.permisson_id
where `ur`.user_id = 1 and `p`.`name` is not null

-- 根据用户id 查询出角色id,然后根据角色id查询出拥有的权限id,再根据权限id查询出可用菜单集合(去重?用户可用有多个角色身份,可能存在权限重复)

PermissionMapper.java

List<String> selectPermsByUserId(@Param("userId") Long userId);

PermissionMapper.xml

<?xm1 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.learnspringsecurity.mapper.PermissionMapper">

    <select id="selectPermsByUserId" resultType="java.lang.String">
        select distinct `p`.`name`
        from `user_role` ur
            left join `role_permission` rp on `ur`.role_id = `rp`.role_id
            left join `permission` p on `p`.id =  `rp`.permisson_id
        where `ur`.user_id = #{userId} and `p`.`name` is not null
    </select>
</mapper>

给相应接口定义所需要的权限

@RequestMapping("/hi")
@PreAuthorize("hasAnyAuthority('hi')")
public String hello(){
    return "hi ~ ";
}

UserDetailServiceImpl.loadUserByUsername

List<String> list = permissionMapper.selectPermsByUserId(user.getId());
// 把数据封装成UserDetail并返回
return new LoginUser(user,list);

自定义失败处理

在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败

还是授权失败出现的异常。

如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即呵。

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        // 授权失败 异常处理
        Ret<String> ret = new Ret<>(HttpStatus.FORBIDDEN.value(),"当前用户没有权限执行该操作",null);
        String jsonString = JSON.toJSONString(ret);
        WebUtil.renderString(httpServletResponse, jsonString);
    }
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        // 认证失败 异常处理
        Ret<String> ret = new Ret<>(HttpStatus.UNAUTHORIZED.value(),"用户认证失败请重新登录",null);
        String jsonString = JSON.toJSONString(ret);
        WebUtil.renderString(httpServletResponse, jsonString);
    }
}

SecurityConfig.configure

// 认证/授权 失败处理器
http.exceptionHandling()
        .authenticationEntryPoint(authenticationEntryPoint)
        .accessDeniedHandler(accessDeniedHandler);

跨域

@Configuration
public class CrosConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // 允许跨域请求
        registry.addMapping("/**")  // 允许的路径
                .allowedOrigins("*")  // 允许的域名
                .allowCredentials(true)  // 允许cookie?
                .allowedMethods("GET","POST","DELETE","PUT");// 允许的请求方式
    }
}

SecurityConfig.configure

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

权限校验方法

public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
    protected final Authentication authentication;
    private AuthenticationTrustResolver trustResolver;
    private RoleHierarchy roleHierarchy;
    private Set<String> roles;
    private String defaultRolePrefix = "ROLE_";
    public final boolean permitAll = true;
    public final boolean denyAll = false;
    private PermissionEvaluator permissionEvaluator;
    public final String read = "read";
    public final String write = "write";
    public final String create = "create";
    public final String delete = "delete";
    public final String admin = "administration";
    public SecurityExpressionRoot(Authentication authentication) {
        if (authentication == null) {
            throw new IllegalArgumentException("Authentication object cannot be null");
        } else {
            this.authentication = authentication;
        }
    }
    public final boolean hasAuthority(String authority) {
        return this.hasAnyAuthority(authority);
    }
    public final boolean hasAnyAuthority(String... authorities) {
        return this.hasAnyAuthorityName((String)null, authorities);
    }
    public final boolean hasRole(String role) {
        return this.hasAnyRole(role);
    }
    public final boolean hasAnyRole(String... roles) {
        return this.hasAnyAuthorityName(this.defaultRolePrefix, roles);
    }
    private boolean hasAnyAuthorityName(String prefix, String... roles) {
        Set<String> roleSet = this.getAuthoritySet();
        String[] var4 = roles;
        int var5 = roles.length;
        for(int var6 = 0; var6 < var5; ++var6) {
            String role = var4[var6];
            String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
            if (roleSet.contains(defaultedRole)) {
                return true;
            }
        }
        return false;
    }
    public final Authentication getAuthentication() {
        return this.authentication;
    }
    public final boolean permitAll() {
        return true;
    }
    public final boolean denyAll() {
        return false;
    }
    public final boolean isAnonymous() {
        return this.trustResolver.isAnonymous(this.authentication);
    }
    public final boolean isAuthenticated() {
        return !this.isAnonymous();
    }
    public final boolean isRememberMe() {
        return this.trustResolver.isRememberMe(this.authentication);
    }
    public final boolean isFullyAuthenticated() {
        return !this.trustResolver.isAnonymous(this.authentication) && !this.trustResolver.isRememberMe(this.authentication);
    }
    public Object getPrincipal() {
        return this.authentication.getPrincipal();
    }
    public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
        this.trustResolver = trustResolver;
    }
    public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
        this.roleHierarchy = roleHierarchy;
    }
    public void setDefaultRolePrefix(String defaultRolePrefix) {
        this.defaultRolePrefix = defaultRolePrefix;
    }
    private Set<String> getAuthoritySet() {
        if (this.roles == null) {
            Collection<? extends GrantedAuthority> userAuthorities = this.authentication.getAuthorities();
            if (this.roleHierarchy != null) {
                userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
            }
            this.roles = AuthorityUtils.authorityListToSet(userAuthorities);
        }
        return this.roles;
    }
    public boolean hasPermission(Object target, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, target, permission);
    }
    public boolean hasPermission(Object targetId, String targetType, Object permission) {
        return this.permissionEvaluator.hasPermission(this.authentication, (Serializable)targetId, targetType, permission);
    }
    public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
        this.permissionEvaluator = permissionEvaluator;
    }
    private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
        if (role == null) {
            return role;
        } else if (defaultRolePrefix != null && defaultRolePrefix.length() != 0) {
            return role.startsWith(defaultRolePrefix) ? role : defaultRolePrefix + role;
        } else {
            return role;
        }
    }
}

自定义权限校验方法

@Component("customExpressionRoot")
public class CustomExpressionRoot {

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

在SPEL表达式中使用@ex相当于获取容器中bean的名字为customExpressionRoot的对象。然后再调用这个对象的hasAuthority方法

@RequestMapping("/custom")
@PreAuthorize("@customExpressionRoot.hasAuthority('custom')")
public Ret custom(){
    return Ret.ok();
}

通过配置文件

接口路径与所需权限

HttpSecurity.antMatchers("/hi").hasAuthority("hi")

CSRF

CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

https://blog.csdn.net/freeking101/article/details/86537087

SpringSecurity却防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf. token,前端发起请求的时候需要携带这个csrf_ token,后端会有过滤器进行校验,

如果没有携带或者是伪造的就不允许访问。我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,

而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

认证成功处理

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(authenticationSuccessHandler);
        http.authorizeRequests().anyRequest().authenticated();
    }
    /**
     * 作为bean放入容器中
     * @return AuthenticationManager
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("认证成功了");
        System.out.println(authentication.getPrincipal().toString());
    }
}

当提交这个表单

输出

认证成功了
LoginUser(...

认证失败处理器

@Component
public class AuthenticationFailureHandlerImpl implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        System.out.println("认证失败了");
    }
}
...
@Resource
private AuthenticationFailureHandler authenticationFailureHandler;
...
HttpSecurity.failureHandler(authenticationFailureHandler);

登出成功处理器

HttpSecurity.logout().logoutSuccessHandler(logoutSuccessHandler);
@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("登出成功");
   }
}
posted @ 2022-12-10 23:16  夏末秋初~  阅读(63)  评论(0编辑  收藏  举报