Spring Security用户认证

目录

1、用户认证

1. 需求分析

2、连接用户中心数据库

1. 连接数据库认证

2、扩展用户身份信息

如何扩展Spring Security的用户身份信息呢?

3、资源服务获取用户身份


1、用户认证

1. 需求分析

至此我们了解了使用Spring Security进行认证授权的过程,本节实现用户认证功能。

目前各大网站的认证方式非常丰富:账号密码认证、手机验证码认证、扫码登录等。

这里有不懂的可以去看:OAuth2认证流程_Relievedz的博客-CSDN博客 

2、连接用户中心数据库

1. 连接数据库认证

基于的认证流程在研究Spring Security过程中已经测试通过,到目前为止用户认证流程如下:

认证所需要的用户信息存储在用户中心数据库,现在需要将认证服务连接数据库查询用户信息。

在研究Spring Security的过程中是将用户信息硬编码,如下:

//配置用户信息服务
@Bean
public UserDetailsService userDetailsService() {
    //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
    InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
    manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
    manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
    return manager;
}

我们要认证服务中连接用户中心数据库查询用户信息。

如何使用Spring Security连接数据库认证吗?

前边学习Spring Security工作原理时有一张执行流程图,如下图:

用户提交账号和密码由DaoAuthenticationProvider调用UserDetailsService的loadUserByUsername()方法获取UserDetails用户信息。

查询DaoAuthenticationProvider的源代码如下:

UserDetailsService是一个接口,如下:这个代码是框架的源码,不需要自己编写

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
    UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

 UserDetails是用户信息接口: 这个代码是框架的源码,不需要自己编写

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们只要实现UserDetailsService 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可。

首先屏蔽原来定义的UserDetailsService。 :这是自己写的代码

    //配置用户信息服务
//    @Bean
//    public UserDetailsService userDetailsService() {
//        //这里配置用户信息,这里暂时使用这种方式将用户存储在内存中
//        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
//        manager.createUser(User.withUsername("zhangsan").password("123").authorities("p1").build());
//        manager.createUser(User.withUsername("lisi").password("456").authorities("p2").build());
//        return manager;
//    }

下边自定义UserDetailsService   自己编写

package com.xuecheng.ucenter.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.po.XcUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;

/**
 * @program: xuecheng-plus-project148
 * @description: 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可。
 * @author: Mr.Zhang
 * @create: 2023-03-17 08:52
 **/
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper userMapper;

    /**
     * 根据账号查询用户信息
     * @param s  账号
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //账号
        String username = s;
        //根据username账号查询数据库
        XcUser xcUser = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));

        //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
        if (xcUser==null){
            return null;
        }
        //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码对比
        String password = xcUser.getPassword();
        // //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"test"};
        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        UserDetails userDetails = User.withUsername(username).password(password).authorities(authorities).build();

        return userDetails;
    }
}

写到这里我们需要清楚框架调用loadUserByUsername()方法拿到用户信息之后是如何执行的,见下图:

数据库中的密码加过密的,用户输入的密码是明文,我们需要修改密码格式器PasswordEncoder,原来使用的是NoOpPasswordEncoder,它是通过明文方式比较密码,现在我们修改为BCryptPasswordEncoder,它是将用户输入的密码编码为BCrypt格式与数据库中的密码进行比对。

如下:

    @Bean
    public PasswordEncoder passwordEncoder() {
//        //密码为明文方式
//        return NoOpPasswordEncoder.getInstance();
        return new BCryptPasswordEncoder();
    }

我们通过测试代码测试BCryptPasswordEncoder,如下:

//进行密码比对
public static void main(String[] args) {
    String password = "111111";
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    for (int i = 0; i < 5; i++) {
        //生成密码
        String encode = passwordEncoder.encode(password);
        System.out.println(encode);
        //校验密码,参数1是输入的明文,参数2是正确密码加密后的串
        boolean matches = passwordEncoder.matches(password, encode);
        System.out.println(matches);
    }
    boolean matches = passwordEncoder.matches("1111", "$2a$10$Q0ItVMXc/VwrlRE7NGBFtetut6o7vxpSjQDcKvtakIrCnxOWf.LV.");
    System.out.println(matches);
}

修改数据库中的密码为Bcrypt格式,并且记录明文密码,稍后申请令牌时需要。

由于修改密码编码方式还需要将客户端的密钥更改为Bcrypt格式.

//客户端详情服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("XcWebApp")// client_id
//                .secret("XcWebApp")//客户端密钥
                .secret(new BCryptPasswordEncoder().encode("XcWebApp"))//客户端密钥
                .resourceIds("xuecheng-plus")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //客户端接收授权码的重定向地址
                .redirectUris("http://www.xuecheng-plus.com")
        ;
    }

现在重启认证服务。

下边使用httpclient进行测试:

### 密码模式
POST {
   
   {auth_host}}/auth/oauth/token?client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=stu2&password=111111

参数介绍:

### 密码模式
POST {
  
  {服务器的端口号}}/auth/oauth/token?   路径client_id=XcWebApp&client_secret=XcWebApp&grant_type=password&username=t1(我数据库的用户名)&password=111111(数据库的密码)

输入正确的账号和密码,申请令牌成功。

输入错误的密码,报错:

{
  "error": "invalid_grant",
  "error_description": "用户名或密码错误"
}

输入错误的账号,报错:

{
  "error": "unauthorized",
  "error_description": "UserDetailsService returned null, which is an interface contract violation"
}

 输入正确:

2、扩展用户身份信息

用户表中存储了用户的账号、手机号、email,昵称、qq等信息,UserDetails接口只返回了username、密码等信息,如下:

public interface UserDetails extends Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    String getPassword();

    String getUsername();

    boolean isAccountNonExpired();

    boolean isAccountNonLocked();

    boolean isCredentialsNonExpired();

    boolean isEnabled();
}

我们需要扩展用户身份的信息,在jwt令牌中存储用户的昵称、头像、qq等信息。

如何扩展Spring Security的用户身份信息呢?

在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户信息的。由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息,这里有两个思路:第一是可以扩展UserDetails,使之包括更多的自定义属性,第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。相比较而言,方案二比较简单还不用破坏UserDetails的结构,我们采用方案二。

修改UserServiceImpl如下:

package com.xuecheng.ucenter.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.xuecheng.ucenter.mapper.XcUserMapper;
import com.xuecheng.ucenter.model.po.XcUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;

/**
 * @program: xuecheng-plus-project148
 * @description: 接口查询数据库得到用户信息返回UserDetails 类型的用户信息即可。
 * @author: Mr.Zhang
 * @create: 2023-03-17 08:52
 **/
@Slf4j
@Component
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    XcUserMapper userMapper;

    /**
     * 根据账号查询用户信息
     * @param s  账号
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //账号
        String username = s;
        //根据username账号查询数据库
        XcUser xcUser = userMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, username));

        //查询到用户不存在,要返回null即可,spring security框架抛出异常用户不存在
        if (xcUser==null){
            return null;
        }
        //如果查到了用户拿到正确的密码,最终封装成一个UserDetails对象给spring security框架返回,由框架进行密码对比
        String password = xcUser.getPassword();
        // //用户权限,如果不加报Cannot pass a null GrantedAuthority collection
        String[] authorities= {"test"};
        xcUser.setPassword(null);
        //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入
        //将用户信息转json
        String userJson = JSON.toJSONString(xcUser);
        UserDetails userDetails = User.withUsername(userJson).password(password).authorities(authorities).build();

        return userDetails;
    }
}

重启认证服务,重新生成令牌,生成成功。

我们可以使用check_token查询jwt的内容

###校验jwt令牌
POST {
   
   {auth_host}}/auth/oauth/check_token?token=

响应示例如下,

{
  "aud": [
    "xuecheng-plus"
  ],
  "user_name": "{\"createTime\":\"2022-09-28T08:32:03\",\"id\":\"51\",\"name\":\"学生2\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"stu2\",\"utype\":\"101001\"}",
  "scope": [
    "all"
  ],
  "active": true,
  "exp": 1679025018,
  "authorities": [
    "test"
  ],
  "jti": "14c4085b-7787-4ce8-8c24-90f1fc80df34",
  "client_id": "XcWebApp"
}

user_name存储了用户信息的json格式,在资源服务中就可以取出该json格式的内容转为用户对象去使用。

3、资源服务获取用户身份

下边编写一个工具类在各个微服务中去使用,获取当前登录用户的对象。

package com.xuecheng.content.util;

import com.alibaba.fastjson.JSON;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.SecurityContextHolder;

import java.io.Serializable;
import java.time.LocalDateTime;


/**
 * @program: xuecheng-plus-project148
 * @description: 获取当前用户身份工具类
 * @author: Mr.Zhang
 * @create: 2023-03-17 10:05
 **/
@Slf4j
public class SecurityUtil {

    public static XcUser getUser() {
        try {
            Object principalObj = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            if (principalObj instanceof String) {
                //取出用户身份信息
                String principal = principalObj.toString();
                //将json转成对象
                XcUser user = JSON.parseObject(principal, XcUser.class);
                return user;
            }
        } catch (Exception e) {
            log.error("获取当前登录用户身份出错:{}", e.getMessage());
            e.printStackTrace();
        }

        return null;
    }


    @Data
    public static class XcUser implements Serializable {

        private static final long serialVersionUID = 1L;

        private String id;

        private String username;

        private String password;

        private String salt;

        private String name;
        private String nickname;
        private String wxUnionid;
        private String companyId;
        /**
         * 头像
         */
        private String userpic;

        private String utype;

        private LocalDateTime birthday;

        private String sex;

        private String email;

        private String cellphone;

        private String qq;

        /**
         * 用户状态
         */
        private String status;

        private LocalDateTime createTime;

        private LocalDateTime updateTime;


    }


}

下边在内容管理服务中测试此工具类,以查询课程信息接口为例:

  @ApiOperation("根据id查询课程")
    @GetMapping("/course/{courseId}")
    public CourseBaseInfoDto getCourseBaseById(@PathVariable Long courseId){
        //获取当前用户的身份
//        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
//        System.out.println("身份验证"+principal);
        SecurityUtil.XcUser user = SecurityUtil.getUser();
        System.out.println(user.getUsername());
        CourseBaseInfoDto courseBaseInfo = courseBaseInfoService.getCourseBaseInfo(courseId);
        return courseBaseInfo;
    }

重启内容管理服务:

1、启动认证服务、网关、内容管理服务

2、生成新的令牌

3、携带令牌访问内容管理服务的查询课程接口

猜你喜欢

转载自blog.csdn.net/Relievedz/article/details/129611666