Skip to content

互联网安全

github

安全三要素

1、保密性:数据内容不能泄露。加密是实现机密性要求的常用手段。

2、完整性:数据内容是完整的,没有被篡改。数字签名是常见的保证一致性的手段。

3、可用性:资源是“随需而得”。拒绝服务攻击就是破话安全的可用性。

SQL注入防御

  1. 使用预编译语句 防御SQL注入的最佳方式就是使用预编译语句,绑定变量。

  2. 检查数据类型 检查输入数据的数据类型,在很大程度上可以对抗SQL注入

异常抛出敏感数据信息

当异常会被传递到信任边界以外时,必须同时对敏感的异常消息和敏感的异常类型进行过滤。

common包下建ErrorControllerAdvice类,类上加@ControllerAdvice注解,方法上加@ExceptionHandler(**.class),返回自定义的error信息

java
@ControllerAdvice
public class ErrorControllerAdvice {

    // 运行时异常
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody
    public Result handlerRuntimeException(RuntimeException ex, HandlerMethod hm){
        //return Result.error(2, "系统异常");
        return Result.error(2, ex.toString());
    }

    // 自定义异常
    @ExceptionHandler(CustException.class)
    @ResponseBody
    public Result handlerError(CustException ex, HandlerMethod hm){
        return Result.error(1, ex.getMessage());
    }

    // 其他异常
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result handleError(Exception ex, HandlerMethod hm) {
        return Result.error(1, "系统异常");
    }

}
java
/**
 * 自定义异常类
 */
public class CustException extends RuntimeException{

    public CustException() {
        super();
    }

    public CustException(String message) {
        super(message);
    }
}
java
// service层使用
 public String login(String name, String pwd) {
      pwd = DigestUtil.md5Hex(pwd + SALT);
      User user = userInfoMapper.login(name, pwd);
      if (null == user) {
          throw new CustException("用户或密码不存在。");
      }
 }

认证和授权

认证

注册和登录传输密码时,加随机信息后加密。

pwd = DigestUtil.md5Hex(pwd + SALT) 加盐防止反推出密码

controller层进行token校验,自定义注解@TokenVerify,通过切面,实现校验逻辑。

java
/**
 * 验证token注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TokenVerify {
}
java
@Aspect
@Component
public class TokenAspect {

    private final static Logger log = LoggerFactory.getLogger(TokenAspect.class);

    @Autowired
    UserInfoService userInfoService;

    @Autowired
    RoleService roleService;


    @Pointcut("@annotation(com.test.websecurity.common.TokenVerify)")
    public void tokenAspect() {

    }

    @Around("tokenAspect()")
    public Object beforePointcut(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("  切面类  ---!");

        //获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        //1、验证参数Authorization是否存在
        String token = request.getHeader("Authorization");
        if (ObjectUtils.isEmpty(token)){
            return Result.error(1,"验证token失败");
        }


        //2、验证参数Authorization是否合法
        User user = userInfoService.getTokenMap().get(token);
        if (ObjectUtils.isEmpty(user)){
            return Result.error(2,"验证token失败");
        }

        //验证完成后调用 触发aop前置的方法 并返回处理完成的结果
        Object result = joinPoint.proceed();
        return result;
    }
}
java
@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserInfoService userInfoService;

    @TokenVerify
    @GetMapping("getUser")
    public Result getUser(@RequestParam Integer userId) {

        User user = userInfoService.getUserInfo(userId);
        return Result.OK().data(user);
    }

    @TokenVerify
    @GetMapping("getUserList")
    public Result getUserLst() {
        List<User> userList = userInfoService.getUserList();
        return Result.OK().data(userList);
    }
}

访问控制

在Web应用中,根据客体的不同,常见的访问控制可以分为:

  1. 基于URl的访问控制
  2. 基于方法的访问控制
  3. 基于数据的访问控制

访问控制-垂直权限管理

基于角色的访问控制(Role-Based Access Control)”,简称RBAC。 alt text

不同角色的权限有高低之分。 高权限的角色往往可以访问低权限角色的资源,低权限角色访问高权限角色的资源往往是被禁止的。 一个低权限的用户通过一些方法能够获取高权限角色的能力,我们称之为“越权”。

同样地,在controller层方法上通过切面校验查询用户角色以及该角色允许访问的URI列表来鉴权。

java
/**
 * 角色服务
 */
public interface RoleService {

    /**
     * 获取用户对应的角色
     * @param userId
     * @return
     */
    Integer getUserRoleId(Integer userId);


    /**
     * 获取角色对应的操作权限
     * @param roleId
     * @return
     */
    List<String> getRoleOperationList(Integer roleId);
}
java
/**
 * 角色服务
 */
@Service
public class RoleServiceImpl implements RoleService {

    @Autowired
    RoleMapper roleMapper;

    @Override
    public Integer getUserRoleId(Integer userId) {
        return roleMapper.getUserRoleId(userId);
    }

    @Override
    public List<String> getRoleOperationList(Integer roleId) {
        return roleMapper.getRoleOperationList(roleId);
    }
}
java
@Aspect
@Component
public class TokenAspect {

    private final static Logger log = LoggerFactory.getLogger(TokenAspect.class);

    @Autowired
    RoleService roleService;

    @Pointcut("@annotation(com.test.websecurity.common.TokenVerify)")
    public void tokenAspect() {

    }

    @Around("tokenAspect()")
    public Object beforePointcut(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("  切面类  ---!");

        //获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        //从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);

        String url = request.getRequestURI();
        if (!allowAccess(user.getId(), url)) {
            return Result.error(3,"对不起,您无权操作。");
        }

        //验证完成后调用 触发aop前置的方法 并返回处理完成的结果
        Object result = joinPoint.proceed();
        return result;
    }


    private Boolean allowAccess(Integer userId, String uri) {
        // 通过用户ID获取用户角色ID
        Integer roleId = roleService.getUserRoleId(userId);

        // 根据角色Id获取该角色允许访问的所有URI列表
        List<String> urlList = roleService.getRoleOperationList(roleId);

        // 检查目标URI是否在允许访问的URI列表中
        return urlList.contains(uri);
    }
}
java
public interface UserInfoService {
    /**
     * 获取用户信息
     * @param userId
     * @return
     */
    User getUserInfo(Integer userId);


    /**
     * 登录接口
     * @param name
     * @param pwd
     * @return
     */
    String login(String name, String pwd);

    /**
     * 抛异常的登录接口
     * @param name
     * @param pwd
     * @return
     */
    String loginException(String name, String pwd);

    /**
     * 注册用户
     * @param name
     * @param pwd
     * @param roleId
     */
    void register(String name, String pwd,  Integer roleId);

    /**
     * 获取token map
     * @return
     */
     Map<String, User> getTokenMap();

    /**
     * 获取用户列表
     * @return
     */
     List<User> getUserList();
}
java
/**
 * 账户服务
 */
@Service
public class UserInfoServiceImpl implements UserInfoService {

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Autowired
    private RoleMapper roleMapper;

    /**
     * key:token; value:user
     */
    private Map<String, User> tokenMap = new ConcurrentHashMap<>();

    /**
     * key:userId; value:token
     */
    private Map<Integer, String> userTokenMap = new ConcurrentHashMap<>();

    private static final String SALT = "S7T*";

    @Override
    public User getUserInfo(Integer userId) {
        User user = userInfoMapper.selectInfo(userId);
        //User select = userInfoMapper.selectInfoAuth(userId);
        System.out.println(user);
        return user;
    }

    @Override
    public List<User> getUserList() {
        return userInfoMapper.selectUserList();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void register(String name, String pwd, Integer roleId) {
        User user = new User();
        user.setName(name);
        pwd = DigestUtil.md5Hex(pwd + SALT);
        user.setPwd(pwd);

        userInfoMapper.register(user);
        if (null != roleId) {
            roleMapper.addUserRole(user.getId(), roleId);
        } else {
            //新注册用户默认是1:普通用户
            roleMapper.addUserRole(user.getId(), 1);
        }
    }

    /**
     *
     * @param name
     * @param pwd
     * @return
     */
    @Override
    public String loginException(String name, String pwd) {
        User user = userInfoMapper.loginException(name, pwd);
        if (null == user) {
            throw new CustException("用户或密码不存在。");
        }

        String tokenOld = userTokenMap.get(user.getId());
        if (StrUtil.isNotBlank(tokenOld)) {
            tokenMap.remove(tokenOld);
        }
        String token = String.valueOf(System.nanoTime());
        userTokenMap.put(user.getId(), token);
        tokenMap.put(token, user);

        return token;
    }

    @Override
    public String login(String name, String pwd) {
        pwd = DigestUtil.md5Hex(pwd + SALT);
        User user = userInfoMapper.login(name, pwd);
        if (null == user) {
            throw new CustException("用户或密码不存在。");
        }

        String tokenOld = userTokenMap.get(user.getId());
        if (StrUtil.isNotBlank(tokenOld)) {
            tokenMap.remove(tokenOld);
        }

        String token = String.valueOf(System.nanoTime());
        userTokenMap.put(user.getId(), token);
        tokenMap.put(token, user);

        return token;
    }


    @Override
    public Map<String, User> getTokenMap() {
        return tokenMap;
    }
}

访问控制-水平权限管理

由于水平权限管理是系统缺乏一个数据级的访问控制造成的,因此水平权限管理又称之为“基于数据的访问控制” alt text

校验controller层方法中传入的参数userIdThreadLocal中保存的是否一致(在切面校验中根据token识别出userId)。

线程共享变量解决了在接口中加额外参数的问题。

java
/**
 * token holder
 */
public class RequestHolder {

    private final static ThreadLocal<Integer> requestHolder = new ThreadLocal<>();
    public static void set(Integer id) {
        requestHolder.set(id);
    }

    public static Integer getId() {
        return requestHolder.get();
    }
}
java
@Aspect
@Component
public class TokenAspect {

    private final static Logger log = LoggerFactory.getLogger(TokenAspect.class);

    @Pointcut("@annotation(com.test.websecurity.common.TokenVerify)")
    public void tokenAspect() {

    }

    @Around("tokenAspect()")
    public Object beforePointcut(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("  切面类  ---!");

        //获取RequestAttributes
        
        //从获取RequestAttributes中获取HttpServletRequest的信息
        
        //1、验证参数Authorization是否存在
        
        //2、验证参数Authorization是否合法
        
        RequestHolder.set(user.getId());

        //验证完成后调用 触发aop前置的方法 并返回处理完成的结果
        Object result = joinPoint.proceed();
        return result;
    }
}
java
@RestController
@RequestMapping("user")
public class UserController {

    @Autowired
    private UserInfoService userInfoService;

    @TokenVerify
    @GetMapping("getUser")
    public Result getUser(@RequestParam Integer userId) {

        if (!RequestHolder.getId().equals(userId)) {
            throw new CustException("您无权访问该数据");
        }

        User user = userInfoService.getUserInfo(userId);
        return Result.OK().data(user);
    }
}

加密

数据加密的基本过程,就是对原来为明文的文件或数据按某种算法进行处理,使其成为不可读的一段代码,通常称为“密文”。通过这样的途径,来达到保护数据不被非法人窃取、阅读的目的。

加密的逆过程为解密,即将该编码信息转化为其原来数据的过程。

加密算法-对称加密

通讯双方使用共享的相同密钥对消息进行加密和解密操作的密码算法。 在对称加密算法中,使用的密钥只有一个,这就要求加密和解密方事先都必须知道加密的密钥

alt text

java
/**
 * 对称加密算法测试类
 *1
 */
public class SymmetricCryptoTest {

    public static void main(String[] args) {
        SymmetricCryptoTest.aesTest();
        SymmetricCryptoTest.desTest();
        SymmetricCryptoTest.sm4Test();
    }

    private static void aesTest() {
        String content = "这是一个待加密的信息";
        System.out.println("aes待加密信息:" + content);

        AES aes = new AES("CBC", "PKCS7Padding",
                // 密钥,可以自定义
                "0123456789ABHAEQ".getBytes(),
                // iv加盐,按照实际需求添加
                "DYgjCEIMVrj2W9xN".getBytes());

        // 加密为16进制表示
        String encryptHex = aes.encryptHex(content);
        System.out.println("aes加密结果:" + encryptHex);

        // 解密
        String decryptStr = aes.decryptStr(encryptHex);
        System.out.println("aes解密结果:" + decryptStr);
    }

    private static void desTest() {
        String content = "这是一个待加密的信息";
        System.out.println("des待加密信息:" + content);

        byte[] key = SecureUtil.generateKey(SymmetricAlgorithm.DESede.getValue()).getEncoded();
        System.out.println("des秘钥:" + new String(key));

        SymmetricCrypto des = new SymmetricCrypto(SymmetricAlgorithm.DESede, key);

        //加密
        byte[] encrypt = des.encrypt(content);

        //解密
        byte[] decrypt = des.decrypt(encrypt);

        //加密为16进制字符串(Hex表示)
        String encryptHex = des.encryptHex(content);
        System.out.println("des加密结果:" + encryptHex);

        //解密为字符串
        String decryptStr = des.decryptStr(encryptHex);
        System.out.println("des解密结果:" + decryptStr);
    }

    private static void sm4Test() {
        String content = "这是一个待加密的信息";
        System.out.println("sm4待加密信息:" + content);
        SymmetricCrypto sm4 = new SymmetricCrypto("SM4");

        String encryptHex = sm4.encryptHex(content);
        System.out.println("sm4加密结果:" + encryptHex);

        String decryptStr = sm4.decryptStr(encryptHex, CharsetUtil.CHARSET_UTF_8);
        System.out.println("sm4解密结果:" + decryptStr);
    }

}

加密算法-非对称加密

通讯双方使用一对相关却无法推算的密钥对对消息进行加密和解密操作的密码算法。 它需要两个密钥,一个称公开密钥 (public key),即公钥,另一个称为私有密钥 (private key),即私钥。 因为加密和解密使用的是两个不同的密钥,所以这种算法称为 非对称加密算法。

alt text

java
public class AsymmetricCryptoTest {


    public static void main(String[] args) throws NoSuchAlgorithmException {
        AsymmetricCryptoTest.rsaTest();

        AsymmetricCryptoTest.genKeyPair();
    }

    private static void rsaTest() {


        RSA rsa = new RSA();

        //获得私钥
        rsa.getPrivateKey();
        rsa.getPrivateKeyBase64();

        //获得公钥
        rsa.getPublicKey();
        rsa.getPublicKeyBase64();

        //公钥加密,私钥解密
        byte[] encrypt = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey);
        byte[] decrypt = rsa.decrypt(encrypt, KeyType.PrivateKey);

        System.out.println(StrUtil.str(decrypt, CharsetUtil.CHARSET_UTF_8));

        //私钥加密,公钥解密
        byte[] encrypt2 = rsa.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PrivateKey);
        byte[] decrypt2 = rsa.decrypt(encrypt2, KeyType.PublicKey);

        System.out.println(StrUtil.str(encrypt2, CharsetUtil.CHARSET_UTF_8));
    }

    /**
     * RSA:随机生成密钥对
     *
     * @throws NoSuchAlgorithmException
     */
    public static void genKeyPair() throws NoSuchAlgorithmException {
        // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");

        // 初始化密钥对生成器,密钥大小为96-2048位
        keyPairGen.initialize(2048, new SecureRandom());

        // 生成一个密钥对,保存在keyPair中
        KeyPair keyPair = keyPairGen.generateKeyPair();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();    // 得到私钥
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();        // 得到公钥

        // 得到公钥字符串
        String publicKeyStr = new String(Base64.encodeBase64(publicKey.getEncoded()));
        // 得到私钥字符串
        String privateKeyStr = new String(Base64.encodeBase64((privateKey.getEncoded())));

        System.out.println("随机生成的公钥为:" + publicKeyStr);
        System.out.println("随机生成的私钥为:" + privateKeyStr);
    }
}

加密算法-散列算法

指输入任意长度数据即产生固定长度的散列值的密码算法,在这种算法中,数据的任何微小变动都会产生截然不同的散列值。

java
/**
 *
 *  摘要算法测试类
 * 参考 https://www.hutool.cn/docs/#/crypto/%E6%A6%82%E8%BF%B0
 */
public class DigesterTest {

    public static void main(String[] args) {
        //摘要算法md5、sha256、sm3
        DigesterTest.md5Test();
        DigesterTest.sha1Test();
        DigesterTest.sm3Test();
    }

    public static void md5Test() {

        String testStr = "这是一个待加密的信息";
        System.out.println("md5:" + DigestUtil.md5Hex(testStr));

        Digester md5 = new Digester(DigestAlgorithm.MD5);
        int count = 10000000;
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            String digestHex = md5.digestHex(testStr);
        }

        System.out.println("md5Test cost:" + (System.currentTimeMillis() - startTime));
    }

    public static void sha1Test() {
        String testStr = "这是一个待加密的信息";
        System.out.println("sha1:" + DigestUtil.sha256Hex(testStr));

        Digester sha1 = new Digester(DigestAlgorithm.SHA1);
        int count = 10000000;
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            String digestHex = sha1.digestHex(testStr);
        }

        System.out.println("sha1 cost:" + (System.currentTimeMillis() - startTime));
    }

    public static void sm3Test() {
        String testStr = "这是一个待加密的信息";
        Digester sm3 = new Digester("sm3");
        System.out.println("sm3:" + sm3.digestHex(testStr));

        int count = 10000000;
        Long startTime = System.currentTimeMillis();
        for (int i = 0; i < count; i++) {
            String digestHex = sm3.digestHex(testStr);
        }

        System.out.println("sm3 cost:" + (System.currentTimeMillis() - startTime));
    }
}

数字签名

  • 发送者: 先对信件内容用Hash函数,生成摘要(digest),再用自己的私钥对这个摘要加密,生成数字签名,将签名附在信件内容下边,再用对方的公钥加密后,发送。

  • 接收者: 收到信息后,用自己的私钥解密出内容,再用对方公钥解密出摘要,确认发信人没错,再对信件内容用Hash,将结果与收到的digest对比,证明信件未被修改。

java
@Service
public class TestSystemA {

    @Autowired
    TestSystemB testSystemB;

    private static String PRIVATE_KEY = "MIIEvwIBADANB";

    //system A 的私钥
    private static final RSA rsa_en = new RSA(PRIVATE_KEY,null);

    private static String PUBLIC_KEY_B = "MIIBIjANBgkqh";

    //system B 的公钥
    private static final RSA rsa_en_system_b = new RSA(null,PUBLIC_KEY_B);

    public void testAddSign() {
        //1、构造请求报文
        SignTestBean signTestBean = new SignTestBean();
        signTestBean.setUserName("dev001");
        signTestBean.setAge(28);
        String idCard = rsa_en_system_b.encryptHex("1112222", KeyType.PublicKey);
        signTestBean.setIdCard(idCard);

        //2、使用md5 计算报文摘要
        Digester md5 = new Digester(DigestAlgorithm.MD5);
        String digestHex = md5.digestHex(JSONUtil.parseObj(signTestBean).toString());
        System.out.println(idCard);
        //3、通过RSA私钥对摘要加密
        String sign = rsa_en.encryptHex(digestHex, KeyType.PrivateKey);

        //4、发送请求
        // signTestBean.setAge(18); //在此篡改报文内容
        testSystemB.testValidate(signTestBean, sign);

    }

}
java
@Service
public class TestSystemB {

    private static String PUBLIC_KEY = "MIIBIjANBgkqhkiG9wB";

    // A的公钥
    private static final RSA rsa_de = new RSA(null,PUBLIC_KEY);

    private static String PRIVATE_KEY_B = "MIIEvgIBADANBgkqhk";

    //system B 私钥
    private static final RSA rsa_en_system_b = new RSA(PRIVATE_KEY_B, null);

    public Boolean testValidate(SignTestBean signTestBean, String signEncryptReq) {

        //1、使用RSA公钥解密签名
        String digestReq = rsa_de.decryptStr(signEncryptReq, KeyType.PublicKey);

        //2、使用md5 计算报文摘要
        Digester md5 = new Digester(DigestAlgorithm.MD5);
        String digestHex = md5.digestHex(JSONUtil.parseObj(signTestBean).toString());

        //3、对比签名是否相同
        if (digestHex.equals(digestReq)) {
            System.out.println("签名验证通过,请求未被篡改,可放心使用。");
        } else {
            System.out.println("请求报文疑似被篡改。");
            return false;
        }

        // 4、解密身份证信息
        String idCard = rsa_en_system_b.decryptStr(signTestBean.getIdCard(),KeyType.PrivateKey);
        System.out.println(idCard);

        return true;

    }
}