Spring-Security实战(二)

1.验证码服务

可以新建一个验证码模块,也可以直接在user模块

加入依赖

<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

图片验证码生成器配置

package com.dreams.creation.config;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * Kaptcha图片验证码配置类
 **/
@Configuration
public class KaptchaConfig {

    //图片验证码生成器,使用开源的kaptcha
    @Bean
    public DefaultKaptcha producer() {
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "10");
        properties.put("kaptcha.textproducer.char.length","4");
        properties.put("kaptcha.image.height","34");
        properties.put("kaptcha.image.width","138");
        properties.put("kaptcha.textproducer.font.size","25");

        properties.put("kaptcha.noise.impl","com.google.code.kaptcha.impl.NoNoise");
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

验证码的两个类

验证码生成结果类

package com.dreams.creation.model.dto.checkCode;

import lombok.Data;

/**
 * 验证码生成结果类
 */
@Data
public class CheckCodeResultDto {

    /**
     * key用于验证
     */
    private String key;

    /**
     * 混淆后的内容
     * 举例:
     * 1.图片验证码为:图片base64编码
     * 2.短信验证码为:null
     * 3.邮件验证码为: null
     * 4.邮件链接点击验证为:null
     * ...
     */
    private String aliasing;
}

验证码生成参数类

package com.dreams.creation.model.dto.checkCode;

import lombok.Data;

/**
 * 验证码生成参数类
 */
@Data
public class CheckCodeParamsDto {

    /**
     * 验证码类型:pic、sms、email等
     */
    private String checkCodeType;

    /**
     * 业务携带参数
     */
    private String param1;
    private String param2;
    private String param3;
}

 

工具类

package com.dreams.creation.utils;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Base64;

public class EncryptUtil {
    private static final Logger logger = LoggerFactory.getLogger(EncryptUtil.class);

    public static String encodeBase64(byte[] bytes){
        String encoded = Base64.getEncoder().encodeToString(bytes);
        return encoded;
    }

    public static byte[]  decodeBase64(String str){
        byte[] bytes = null;
        bytes = Base64.getDecoder().decode(str);
        return bytes;
    }

    public static String encodeUTF8StringBase64(String str){
        String encoded = null;
        try {
            encoded = Base64.getEncoder().encodeToString(str.getBytes("utf-8"));
        } catch (UnsupportedEncodingException e) {
            logger.warn("不支持的编码格式",e);
        }
        return encoded;

    }

    public static String  decodeUTF8StringBase64(String str){
        String decoded = null;
        byte[] bytes = Base64.getDecoder().decode(str);
        try {
            decoded = new String(bytes,"utf-8");
        }catch(UnsupportedEncodingException e){
            logger.warn("不支持的编码格式",e);
        }
        return decoded;
    }

    public static String encodeURL(String url) {
        String encoded = null;
       try {
          encoded =  URLEncoder.encode(url, "utf-8");
       } catch (UnsupportedEncodingException e) {
          logger.warn("URLEncode失败", e);
       }
       return encoded;
    }


    public static String decodeURL(String url) {
        String decoded = null;
       try {
          decoded = URLDecoder.decode(url, "utf-8");
       } catch (UnsupportedEncodingException e) {
          logger.warn("URLDecode失败", e);
       }
       return decoded;
    }

    public static void main(String [] args){
        String str = "abcd{'a':'b'}";
        String encoded = EncryptUtil.encodeUTF8StringBase64(str);
        String decoded = EncryptUtil.decodeUTF8StringBase64(encoded);
        System.out.println(str);
        System.out.println(encoded);
        System.out.println(decoded);

        String url = "== wo";
        String urlEncoded = EncryptUtil.encodeURL(url);
        String urlDecoded = EncryptUtil.decodeURL(urlEncoded);
        
        System.out.println(url);
        System.out.println(urlEncoded);
        System.out.println(urlDecoded);
    }


}

 

验证码接口

package com.dreams.creation.service;


import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;

/**
 * 验证码接口
 */
public interface CheckCodeService {


    /**
     * @description 生成验证码
     * @param checkCodeParamsDto 生成验证码参数
     * @return CheckCodeResultDto 验证码结果
    */
     CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto);

     /**
      * @description 校验验证码
      * @param key
      * @param code
      * @return boolean
     */
    public boolean verify(String key, String code);


    /**
     * @description 验证码生成器
    */
    public interface CheckCodeGenerator{
        /**
         * 验证码生成
         * @return 验证码
         */
        String generate(int length);
    }

    /**
     * @description key生成器
     */
    public interface KeyGenerator{

        /**
         * key生成
         * @return 验证码
         */
        String generate(String prefix);
    }


    /**
     * @description 验证码存储
     */
    public interface CheckCodeStore{

        /**
         * @description 向缓存设置key
         * @param key key
         * @param value value
         * @param expire 过期时间,单位秒
         * @return void
        */
        void set(String key, String value, Integer expire);

        String get(String key);

        void remove(String key);
    }
}

抽象类AbstractCheckCodeService实现验证码接口

package com.dreams.creation.service;

import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

/**
 * 验证码接口
 */
@Slf4j
public abstract class AbstractCheckCodeService implements CheckCodeService {

    protected CheckCodeGenerator checkCodeGenerator;
    protected KeyGenerator keyGenerator;
    protected CheckCodeStore checkCodeStore;

    public abstract void  setCheckCodeGenerator(CheckCodeGenerator checkCodeGenerator);
    public abstract void  setKeyGenerator(KeyGenerator keyGenerator);
    public abstract void  setCheckCodeStore(CheckCodeStore CheckCodeStore);


    /**
     * @description 生成验证公用方法
     * @param checkCodeParamsDto 生成验证码参数
     * @param code_length 验证码长度
     * @param keyPrefix key的前缀
     * @param expire 过期时间
     * @return GenerateResult 生成结果
    */
    public GenerateResult generate(CheckCodeParamsDto checkCodeParamsDto, Integer code_length, String keyPrefix, Integer expire){
        //生成四位验证码
        String code = checkCodeGenerator.generate(code_length);
        log.debug("生成验证码:{}",code);
        //生成一个key
        String key = keyGenerator.generate(keyPrefix);

        //存储验证码
        checkCodeStore.set(key,code,expire);
        //返回验证码生成结果
        GenerateResult generateResult = new GenerateResult();
        generateResult.setKey(key);
        generateResult.setCode(code);
        return generateResult;
    }

    @Data
    protected class GenerateResult{
        String key;
        String code;
    }


    public abstract CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto);


    public boolean verify(String key, String code){
        if (StringUtils.isBlank(key) || StringUtils.isBlank(code)){
            return false;
        }
        String code_l = checkCodeStore.get(key);
        if (code_l == null){
            return false;
        }
        boolean result = code_l.equalsIgnoreCase(code);
        if(result){
            //删除验证码
            checkCodeStore.remove(key);
        }
        return result;
    }


}

使用本地内存存储验证码

package com.dreams.creation.service.impl;

import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;

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

/**
 * 使用本地内存存储验证码
 */
@Component("MemoryCheckCodeStore")
public class MemoryCheckCodeStore implements CheckCodeService.CheckCodeStore {

    Map<String,String> map = new HashMap<String,String>();

    @Override
    public void set(String key, String value, Integer expire) {
        map.put(key,value);
    }

    @Override
    public String get(String key) {
        return map.get(key);
    }

    @Override
    public void remove(String key) {
        map.remove(key);
    }
}

使用redis存储验证码

package com.dreams.creation.service.impl;

import com.dreams.creation.service.CheckCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 使用redis存储验证码
 */
@Component("RedisCheckCodeStore")
public class RedisCheckCodeStore implements CheckCodeService.CheckCodeStore {

    @Autowired
    RedisTemplate redisTemplate;


    @Override
    public void set(String key, String value, Integer expire) {
        redisTemplate.opsForValue().set(key,value,expire, TimeUnit.SECONDS);
    }

    @Override
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    @Override
    public void remove(String key) {
        redisTemplate.delete(key);
    }
}

数字字母生成器

package com.dreams.creation.service.impl;


import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;

import java.util.Random;

/**
 * 数字字母生成器
 */
@Component("NumberLetterCheckCodeGenerator")
public class NumberLetterCheckCodeGenerator implements CheckCodeService.CheckCodeGenerator {


    @Override
    public String generate(int length) {
        String str="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random=new Random();
        StringBuffer sb=new StringBuffer();
        for(int i=0;i<length;i++){
            int number=random.nextInt(36);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }


}

uuid生成器

package com.dreams.creation.service.impl;

import com.dreams.creation.service.CheckCodeService;
import org.springframework.stereotype.Component;

import java.util.UUID;

/**
 * uuid生成器
 */
@Component("UUIDKeyGenerator")
public class UUIDKeyGenerator implements CheckCodeService.KeyGenerator {
    @Override
    public String generate(String prefix) {
        String uuid = UUID.randomUUID().toString();
        return prefix + uuid.replaceAll("-", "");
    }
}

 

接下来就是图片验证码生成服务

package com.dreams.creation.service.impl;

import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import com.dreams.creation.service.AbstractCheckCodeService;
import com.dreams.creation.service.CheckCodeService;
import com.dreams.creation.utils.EncryptUtil;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import sun.misc.BASE64Encoder;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * 图片验证码生成器
 */
@Service("PicCheckCodeService")
public class PicCheckCodeServiceImpl extends AbstractCheckCodeService implements CheckCodeService {


    @Autowired
    private DefaultKaptcha kaptcha;


    @Resource(name="NumberLetterCheckCodeGenerator")
    @Override
    public void setCheckCodeGenerator(CheckCodeGenerator checkCodeGenerator) {
        this.checkCodeGenerator = checkCodeGenerator;
    }

    @Resource(name="UUIDKeyGenerator")
    @Override
    public void setKeyGenerator(KeyGenerator keyGenerator) {
        this.keyGenerator = keyGenerator;
    }


    @Resource(name="RedisCheckCodeStore")
    @Override
    public void setCheckCodeStore(CheckCodeStore checkCodeStore) {
        this.checkCodeStore = checkCodeStore;
    }


    @Override
    public CheckCodeResultDto generate(CheckCodeParamsDto checkCodeParamsDto) {
        GenerateResult generate = generate(checkCodeParamsDto, 4, "checkcode:", 300);
        String key = generate.getKey();
        String code = generate.getCode();
        String pic = createPic(code);
        CheckCodeResultDto checkCodeResultDto = new CheckCodeResultDto();
        checkCodeResultDto.setAliasing(pic);
        checkCodeResultDto.setKey(key);
        return checkCodeResultDto;

    }

    private String createPic(String code) {
        // 生成图片验证码
        ByteArrayOutputStream outputStream = null;
        BufferedImage image = kaptcha.createImage(code);

        outputStream = new ByteArrayOutputStream();
        String imgBase64Encoder = null;
        try {
            // 对字节数组Base64编码
            BASE64Encoder base64Encoder = new BASE64Encoder();
            ImageIO.write(image, "png", outputStream);
            imgBase64Encoder = "data:image/png;base64," + EncryptUtil.encodeBase64(outputStream.toByteArray());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return imgBase64Encoder;
    }
}

 

controller层调用它

package com.dreams.creation.controller;

import com.dreams.creation.model.dto.checkCode.CheckCodeParamsDto;
import com.dreams.creation.model.dto.checkCode.CheckCodeResultDto;
import com.dreams.creation.service.CheckCodeService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * 验证码服务接口
 */
@Api(value = "验证码服务接口")
@RestController
@RequestMapping("/checkCode")
public class CheckCodeController {

    @Resource(name = "PicCheckCodeService")
    private CheckCodeService picCheckCodeService;


    @ApiOperation(value="生成验证信息", notes="生成验证信息")
    @PostMapping(value = "/picCode")
    public CheckCodeResultDto generatePicCheckCode(CheckCodeParamsDto checkCodeParamsDto){
        return picCheckCodeService.generate(checkCodeParamsDto);
    }

    @ApiOperation(value="校验", notes="校验")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "name", value = "业务名称", required = true, dataType = "String", paramType="query"),
            @ApiImplicitParam(name = "key", value = "验证key", required = true, dataType = "String", paramType="query"),
            @ApiImplicitParam(name = "code", value = "验证码", required = true, dataType = "String", paramType="query")
    })
    @PostMapping(value = "/verify/code")
    public Boolean verify(String key, String code){
        Boolean isSuccess = picCheckCodeService.verify(key,code);
        return isSuccess;
    }
}

接下来定义Feign接口

package com.dreams.creation.service;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "dreams-checkCode-service",fallbackFactory = CheckCodeClientFactory.class)
@RequestMapping("/checkCode")
public interface CheckCodeClient {

 @PostMapping(value = "/verify/code")
 public Boolean verify(@RequestParam("key") String key, @RequestParam("code") String code);

}

以及降级逻辑

package com.dreams.creation.service;

import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class CheckCodeClientFactory implements FallbackFactory<CheckCodeClient> {
 @Override
 public CheckCodeClient create(Throwable throwable) {
  return new CheckCodeClient() {

   @Override
   public Boolean verify(String key, String code) {
    log.debug("调用验证码服务熔断异常:{}", throwable.getMessage());
    return null;
   }
  };
 }
}

熔断降级需要在启动类加上注解

@EnableFeignClients(basePackages = {"com.dreams.creation.service"})

 

请求

POST http://localhost:8101/api/user/checkCode/picCode

他会存储到redis里面

请求确认

POST http://localhost:8102/api/user/checkCode/verify/code?key=checkcode:b8b2d66638d4434ca87ba53f8cbd0806&code=7TTT

可以看到返回true

redis里的key就会被删除

 

2.账号名密码方式登录

注入验证码service,实现AuthService,来实现账号名密码方式登录的逻辑

package com.dreams.creation.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.dreams.creation.model.dto.user.AuthParamsDto;
import com.dreams.creation.model.dto.user.DcUserExt;
import com.dreams.creation.model.entity.DcUser;
import com.dreams.creation.service.AuthService;
import com.dreams.creation.service.CheckCodeClient;
import com.dreams.creation.service.UserFeignClient;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

/**
 * 账号名密码方式
 */
@Service("password_authService")
public class PasswordAuthServiceImpl implements AuthService {

    @Autowired
    UserFeignClient userFeignClient;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    CheckCodeClient checkCodeClient;

    @Override
    public DcUserExt execute(AuthParamsDto authParamsDto) {
        //账号
        String userAccount = authParamsDto.getUserAccount();

        //输入的验证码
        String checkcode = authParamsDto.getCheckCode();
        //验证码对应的key
        String checkcodekey = authParamsDto.getCheckCodeKey();

        if (StringUtils.isEmpty(checkcode) || StringUtils.isEmpty(checkcodekey)) {
            throw new RuntimeException("请输入的验证码");
        }

        //远程调用验证码服务接口去校验验证码
        Boolean verify = checkCodeClient.verify(checkcodekey, checkcode);
        if (verify == null || !verify) {
            throw new RuntimeException("验证码输入错误");
        }


        //账号是否存在
        //根据username账号查询数据库
        DcUser dcUser = userFeignClient.selectOne(new LambdaQueryWrapper<DcUser>().eq(DcUser::getUserAccount, userAccount));

        //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
        if (dcUser == null) {
            throw new RuntimeException("账号不存在");
        }

        //验证密码是否正确
        //如果查到了用户拿到正确的密码
        String passwordDb = dcUser.getUserPassword();
        //拿 到用户输入的密码
        String passwordForm = authParamsDto.getUserPassword();
        //校验密码
        boolean matches = passwordEncoder.matches(passwordForm, passwordDb);
        if (!matches) {
            throw new RuntimeException("账号或密码错误");
        }
        DcUserExt dcUserExt = new DcUserExt();
        BeanUtils.copyProperties(dcUser, dcUserExt);

        return dcUserExt;
    }
}

这样UserDetailsServiceImpl里面就可以直接调用了,后面加入微信扫码认证也不需要修改UserDetailsServiceImpl。

package com.dreams.creation.service.impl;

import com.alibaba.fastjson.JSON;
import com.dreams.creation.model.dto.user.AuthParamsDto;
import com.dreams.creation.model.dto.user.DcUserExt;
import com.dreams.creation.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.security.core.userdetails.User;
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;

/**
 * @author PoemsAndDreams
 * @date 2025-01-16 18:57
 * @description //TODO
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    ApplicationContext applicationContext;


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


        AuthParamsDto authParamsDto = null;
        try {
            //将认证参数转为AuthParamsDto类型
            authParamsDto = JSON.parseObject(authParams, AuthParamsDto.class);
        } catch (Exception e) {
            // todo 日志
            throw new RuntimeException("认证请求数据格式不对");
        }
        //认证方法
        String authType = authParamsDto.getAuthType();
        AuthService authService =  applicationContext.getBean(authType + "_authService",AuthService.class);

        DcUserExt user = authService.execute(authParamsDto);

        return getUserPrincipal(user);
    }

    private UserDetails getUserPrincipal(DcUserExt user) {
        //取出数据库存储的正确密码
        String password  =user.getUserPassword();

        //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"test"};
        //为了安全在令牌中不放密码
        user.setUserPassword(null);
        //将user对象转json
        String userString = JSON.toJSONString(user);
        //创建UserDetails对象
        UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();

        return userDetails;
    }
}

 

 

 

3.用户授权

RBAC分为两种方式:

  • 基于角色的访问控制(Role-Based Access Control)
  • 基于资源的访问控制(Resource-Based Access Control)

角色的访问控制(Role-Based Access Control)是按角色进行授权

基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权

基于资源的访问控制系统可扩展性强,所以建议使用基于资源的访问控制

用到的表如下:

--
-- Table structure for table `menu`
--

DROP TABLE IF EXISTS `menu`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `menu`
(
    `id`          varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `code`        varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '菜单编码',
    `p_id`        varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '父菜单ID',
    `menu_name`   varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '名称',
    `url`         varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '请求地址',
    `is_menu`     char(1) CHARACTER SET utf8 COLLATE utf8_general_ci      DEFAULT NULL COMMENT '是否是菜单',
    `level`       int                                                     DEFAULT NULL COMMENT '菜单层级',
    `sort`        int                                                     DEFAULT NULL COMMENT '菜单排序',
    `status`      char(1) CHARACTER SET utf8 COLLATE utf8_general_ci      DEFAULT NULL,
    `icon`        varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
    `create_time` datetime                                                DEFAULT NULL,
    `update_time` datetime                                                DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `FK_CODE` (`code`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb3
  ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;



--
-- Table structure for table `permission`
--

DROP TABLE IF EXISTS `permission`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `permission`
(
    `id`          varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci  NOT NULL,
    `role_id`     varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci  NOT NULL,
    `menu_id`     varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `create_time` datetime DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `permission_unique` (`role_id`, `menu_id`) USING BTREE,
    KEY `permission_menu_id` (`menu_id`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb3
  ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;


--
-- Table structure for table `role`
--

DROP TABLE IF EXISTS `role`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `role`
(
    `id`          varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `role_name`   varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
    `role_code`   varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
    `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
    `create_time` datetime                                                DEFAULT NULL,
    `update_time` datetime                                                DEFAULT NULL,
    `status`      char(1) CHARACTER SET utf8 COLLATE utf8_general_ci     NOT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE KEY `unique_role_name` (`role_name`) USING BTREE,
    UNIQUE KEY `unique_role_value` (`role_code`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb3
  ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;



--
-- Table structure for table `dcUser`
--


CREATE TABLE `dcUser`
(
    `id`           bigint                                                        NOT NULL AUTO_INCREMENT COMMENT 'id',
    `userAccount`  varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号',
    `userPassword` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码',
    `userName`     varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci          DEFAULT NULL COMMENT '用户昵称',
    `userAvatar`   varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci         DEFAULT NULL COMMENT '用户头像',
    `userProfile`  varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci          DEFAULT NULL COMMENT '用户简介',
    `sex`          char(1) CHARACTER SET utf8 COLLATE utf8_general_ci                     DEFAULT NULL,
    `email`        varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci                 DEFAULT NULL,
    `cellphone`    varchar(45) CHARACTER SET utf8 COLLATE utf8_general_ci                 DEFAULT NULL,
    `qq`           varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci                 DEFAULT NULL,
    `editTime`     datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '编辑时间',
    `createTime`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updateTime`   datetime                                                      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `isDelete`     tinyint                                                       NOT NULL DEFAULT '0' COMMENT '是否删除',
    PRIMARY KEY (`id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 7
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT ='用户';



--
-- Table structure for table `user_role`
--

DROP TABLE IF EXISTS `user_role`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `user_role`
(
    `id`          varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
    `user_id`     varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci  DEFAULT NULL,
    `role_id`     varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci  DEFAULT NULL,
    `create_time` datetime                                                DEFAULT NULL,
    `creator`     varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
    PRIMARY KEY (`id`) USING BTREE,
    KEY `user_role_user_id` (`user_id`) USING BTREE,
    KEY `user_role_role_id` (`role_id`) USING BTREE
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb3
  ROW_FORMAT = DYNAMIC;
/*!40101 SET character_set_client = @saved_cs_client */;

在用户模块完成上面表的增删改查后

在认证模块的UserDetailsServiceImpl文件

搜索出来上面表的code字段列表,赋值进去就行了

mapper方法

@Select("SELECT * FROM menu WHERE id IN (SELECT menuId FROM role_menu WHERE roleId IN ( SELECT roleId FROM user_role WHERE userId = #{userId} ))")
List<Menu> selectPermissionByUserId(@Param("userId") String userId);

getUserPrincipal方法

private UserDetails getUserPrincipal(DcUserExt user) {
    //取出数据库存储的正确密码
    String password  =user.getUserPassword();

    //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
    String[] authorities= {"test"};

    //搜索 数据库 拿到 该用户对应角色拥有的权限菜单
    //......
    //根据用户id查询用户的权限
    List<Menu> menus = userFeignClient.selectPermissionByUserId(user.getId().toString());


    if(!Objects.isNull(menus)){
        List<String> permissions =new ArrayList<>();
        menus.forEach(m->{
            //拿到了用户拥有的权限标识符
            permissions.add(m.getCode());
        });
        //将permissions转成数组
        authorities = permissions.toArray(new String[0]);
    }

    //为了安全在令牌中不放密码
    user.setUserPassword(null);
    //将user对象转json
    String userString = JSON.toJSONString(user);
    //创建UserDetails对象
    UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build();

    return userDetails;
}

再在相对应的controller方法加入注解

@PreAuthorize("hasAuthority('code字段值')")

那么只有拥有这个权限的用户才能调用该请求

 

 

4.异常捕获

全局异常捕获

在SpringSecurity出现异常会被ExceptionTranslationFilter捕获到。

认证过程异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
授权过程异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

因为公共模块没有引入security依赖,所以无法定义处理这两个异常,所以我们可以给每一个资源服务定义一个全局异常处理

不过注意Bean名字为资源名+GlobalExceptionHandler

否则会重名Bean

认证流程在网关处理,所以资源服务不会出现认证问题,可以不用定义认证异常

package com.dreams.creation.config;


import com.dreams.creation.common.BaseResponse;
import com.dreams.creation.common.ErrorCode;
import com.dreams.creation.common.ResultUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


/**
 * 全局异常处理器
 */
@RestControllerAdvice
@Slf4j
public class UserGlobalExceptionHandler {

    @ExceptionHandler(AccessDeniedException.class)
    public BaseResponse<?> accessDeniedExceptionHandler(AccessDeniedException e) {
        log.error("AccessDeniedException", e);
        return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, "警告,无权限!");
    }
}

 

 

5.前端请求

这里使用React举例

import Footer from '@/components/Footer';
import {getLoginUserUsingGet, userLoginUsingPost} from '@/services/user/userController';
import {ClockCircleOutlined, LockOutlined, UserOutlined} from '@ant-design/icons';
import {LoginForm, ProFormText} from '@ant-design/pro-components';
import {useEmotionCss} from '@ant-design/use-emotion-css';
import {Helmet, history, useModel} from '@umijs/max';
import {message, Tabs} from 'antd';
import React, {useEffect, useState} from 'react';
import {Link} from 'umi';
import Settings from '../../../../config/defaultSettings';
import axios from "axios";

type AuthParamsDto = {
  userAccount?: string;
  userPassword?: string;
  cellphone?: string;
  checkCode?: string;
  checkCodeKey?: string;
  authType?: string;
};

const Login: React.FC = () => {
  const [type, setType] = useState<string>('account');
  const {initialState, setInitialState} = useModel('@@initialState');
  const containerClassName = useEmotionCss(() => {
    return {
      display: 'flex',
      flexDirection: 'column',
      height: '100vh',
      overflow: 'auto',
      backgroundImage:
        "url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr')",
      backgroundSize: '100% 100%',
    };
  });

  const handleSubmit = async (values: AuthParamsDto) => {
    try {


      // 格式化请求数据
      const requestData = {
        client_id: 'DreamsWebApp',
        client_secret: 'DreamsWebApp',
        grant_type: 'password',
        username: JSON.stringify({
          userAccount: values.userAccount,
          authType: 'password', // 默认使用密码验证
          userPassword: values.userPassword,
          checkCode: values.checkCode,
          checkCodeKey: captchaKey,
        })
      };

      console.log("captchaKey", captchaKey)
      // 发送 POST 请求
      const res = await axios.post('http://localhost:8101/api/auth/oauth/token', null, {
        params: requestData,
      });
      console.log("res", res)
      localStorage.setItem('token', res.data.access_token);  // 假设 token 存储在 localStorage 中

      // // 登录
      // const res = await userLoginUsingPost({
      //   ...values,
      // });

      const  cur  = await getLoginUserUsingGet();

      const defaultLoginSuccessMessage = '登录成功!';

      message.success(defaultLoginSuccessMessage);
      // 保存已登录用户信息
      setInitialState({
        ...initialState,
        currentUser: cur.data,
      });

      const urlParams = new URL(window.location.href).searchParams;
      history.push(urlParams.get('redirect') || '/');
      return;
    } catch (error: any) {
      const defaultLoginFailureMessage = `登录失败 , ${error.response.data.error_description} , 请重试!`;
      message.error(defaultLoginFailureMessage);
      // 自动刷新验证码
      fetchCaptcha();
    }
  };

  const [captchaKey, setCaptchaKey] = useState<string>(''); // 用于保存验证码的 key
  const [captchaImage, setCaptchaImage] = useState<string>(''); // 用于保存验证码的 base64 图像
  const [isLoading, setIsLoading] = useState<boolean>(false); // 控制验证码加载状态

  // 获取验证码
  const fetchCaptcha = async () => {
    try {
      setIsLoading(true);
      // const response = await generatePicCheckCodeUsingPost({}) // 需要调整为实际获取验证码的 API
      //
      const response = await axios.post('http://localhost:8101/api/user/checkCode/picCode'); // 需要调整为实际获取验证码的 API
      const {key, aliasing} = response.data;
      setCaptchaKey(key);
      setCaptchaImage(aliasing);
    } catch (error) {
      message.error('获取验证码失败,请稍后再试!');
    } finally {
      setIsLoading(false);
    }
  };

  // 在组件挂载时请求验证码
  useEffect(() => {
    fetchCaptcha();
  }, []);


  return (
    <div className={containerClassName}>
      <Helmet>
        <title>
          {'登录'}- {Settings.title}
        </title>
      </Helmet>
      <div
        style={{
          flex: '1',
          padding: '32px 0',
        }}
      >
        <LoginForm
          contentStyle={{
            minWidth: 280,
            maxWidth: '75vw',
          }}
          logo={<img alt="logo" style={{height: '100%'}} src="/logo.svg"/>}
          title="Dreams智能创作中心"
          subTitle={'Dreams智能创作中心'}
          initialValues={{
            autoLogin: true,
          }}
          onFinish={async (values) => {
            await handleSubmit(values as AuthParamsDto);
          }}
        >
          <Tabs
            activeKey={type}
            onChange={setType}
            centered
            items={[
              {
                key: 'account',
                label: '账户密码登录',
              },
            ]}
          />
          {type === 'account' && (
            <>
              <ProFormText
                name="userAccount"
                fieldProps={{
                  size: 'large',
                  prefix: <UserOutlined/>,
                }}
                placeholder={'请输入账号'}
                rules={[
                  {
                    required: true,
                    message: '账号是必填项!',
                  },
                ]}
              />
              <ProFormText.Password
                name="userPassword"
                fieldProps={{
                  size: 'large',
                  prefix: <LockOutlined/>,
                }}
                placeholder={'请输入密码'}
                rules={[
                  {
                    required: true,
                    message: '密码是必填项!',
                  },
                ]}
              />
              {/* 显示验证码图片和输入框在同一行 */}
              <div style={{display: 'flex', alignItems: 'center'}}>
                <img src={captchaImage} alt="验证码"
                     onClick={fetchCaptcha} // 点击图片时重新获取验证码
                     style={{width: 100, height: 40, marginRight: 20, marginBottom: 20}}/>
                <ProFormText
                  name="checkCode"
                  fieldProps={{
                    size: 'large',
                    prefix: <ClockCircleOutlined/>,
                  }}
                  placeholder={'请输入验证码'}
                  rules={[
                    {
                      required: true,
                      message: '验证码是必填项!',
                    },
                  ]}
                />
              </div>
            </>
          )}

          <div
            style={{
              marginBottom: 24,
              textAlign: 'right',
            }}
          >
            <Link to="/user/register">用户注册</Link>
          </div>
        </LoginForm>
      </div>
      <Footer/>
    </div>
  );
};
export default Login;

 

主要看下面请求

请求验证码

这里是请求登录

localStorage 是浏览器提供的一种机制,用于在浏览器中存储数据。这些数据会被保留直到显式地删除,即使浏览器关闭后,数据也不会丢失。

localStorage.setItem('token', res.data.access_token);  // 假设 token 存储在 localStorage 中

然后请求获取当前登录用户

存储到全局

access.ts会用到这个状态

 

如果登录失败,需要刷新验证码

 

打开requestConfig.ts或requestErrorConfig.ts修改请求拦截器,让所有请求加上token

// 请求拦截器
requestInterceptors: [
  (config: RequestOptions) => {

    // 获取存储在 localStorage 中的 token(假设 token 存储在 localStorage)
    const token = localStorage.getItem('token');

    // 如果 token 存在,将 token 加入到请求头中
    if (token) {
      config.headers['Authorization'] = `Bearer ${token}`;
    }
    // 拦截请求配置,进行个性化处理。
    return config;
  },
],

如果是Upload组件

const token = localStorage.getItem('token');  // 获取 token
<Upload
    action={`${BACKEND_HOST_LOCAL}api/article/file/upload`}
    listType="picture-card"
    data={{
      biz: 'user_avatar', // 这里可以传递你所需的业务标识,例如“avatar”表示头像上传
    }}
    headers={{
       'Authorization': `Bearer ${token}`,  // 将 token 添加到 Authorization 头
    }}
    beforeUpload={handleBeforeUpload}  // 上传前处理
    fileList={fileList} // 文件列表状态
    maxCount={1} // 限制只能上传一个文件
    withCredentials={true}  // 确保跨域请求时携带 cookies
    onChange={handleChange} // 文件变化时更新状态
>

使用headers传入即可

 

那么还有退出登录也要改一下

从请求后端到localStorage.removeItem(‘token’)

const loginOut = async () => {

  // 请求后端删除session的登录用户
  //await userLogoutUsingPost();

  //将从浏览器的 localStorage 中删除名为 'token' 的项。
  localStorage.removeItem('token');

  const { search, pathname } = window.location;
  const urlParams = new URL(window.location.href).searchParams;
  /** 此方法会跳转到 redirect 参数所在的位置 */
  const redirect = urlParams.get('redirect');
  // Note: There may be security issues, please note
  if (window.location.pathname !== '/user/login' && !redirect) {
    history.replace({
      pathname: '/user/login',
      search: stringify({
        redirect: pathname + search,
      }),
    });
  }
};

 

同样如果头像那里需要登录用户信息

在src/components/RightContent/AvatarDropdown.tsx请求拿到登录用户

const [loading, setLoading] = useState(false);
const [currentUser, setcurrentUser] = useState(); // 初始值设为空数组

const loadData = async () => {
  setLoading(true);
  try {
    const  res  = await getLoginUserUsingGet();
    setcurrentUser(res.data)
  } catch (error: any) {
    message.error('请求失败' + error.message);
  } finally {
    setLoading(false);
  }
};
useEffect(() => {
  loadData();
}, []);

该文件下就可以正常展示了

 

5.参考内容

黑马-学成在线

尚硅谷-尚上优选

官网:https://spring.p2hp.com/projects/spring-security.html

OAuth 2.0:https://datatracker.ietf.org/doc/html/rfc6749

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇