我们认证授权使用springsecurity 和oauth2技术尽心实现具体实现流程见第五章文档,这里就是记录一下我们的技术方案
这是最开始的技术方案,我们通过认证为服务获取令牌然后使用令牌访问微服务,微服务解析令牌即可。但是缺点就是每个微服务都要做同样的操作就要配置同样的东西,因此需要改进
这就是改进的方案,就是使用gateway网关进行认证,微服务校验权限合法性。这样总结下来,网关的作用就是:认证,校验jwt令牌合法性;路由;维护一份白名单用户。
下面我来说一下这个图的具体流程,首先用户通过Nginx访问统一认证入口,这个访问是不需要令牌的,然后统一认证入口输入完用户名和密码之后就会访问我们的认证服务生成jwt令牌,注意访问的网址是我们引入了坐标和配置好东西之后就可以访问并不是我们自己定义的api接口(认证服务的具体配置见文档中)访问api接口就会自动进入到我们Nginx中域名下面配置的路径进行访问,也就是通过Nginx访问了网关,这时候我们网关会直接访问我们的统一认证服务,不需要jwt令牌,因为我们在gateway中配置了白名单网址,我们的统一认证就是白名单中的网址,然后统一认证服务通过我们的用户名从数据库中进行查询,并且返回一个UserDetails对象并且把用户名和数据库中·查询的密码进行封装,这样我们的springscruty就会自动进行密码的验证。当认证成功之后就会给我们用户颁发一个令牌,这个令牌信息存储在安全上下文中,当我们访问我们的微服务的时候我们就会拿着令牌进入网关,网关对认证进行了配置(具体配置流程见文档)简单的说就是在网关中配置了一个拦截器,我们的请求会被拦截,当然是有白名单的请求的,当我们访问微服务的请求被拦截之后我们就会获得我们的jwt令牌,之后对令牌进行验证,如果验证通过就放行,这时候我们就可以访问我们的微服务,如果验证失败则直接在网关就直接提示报错信息。
具体实现流程见第五章文档网关认证。
下面我们来说一下学了springscruty之后我们需要实现的重要技术。
第一个就是我们需要自己实现UserDetailsService
为什么要实现这个类呢。因为上面的方式是我们直接将用户信息注册到了内存中,但是我们的期望是我们经过统一认证之后从数据库中查询用户信息,而不是从内存中进行·查询用户的信息,因此我们要实现UserDetailsService类并且注册成bean。 下面是具体的实现代码
package com.xuecheng.ucenter.service.impl; import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xuecheng.ucenter.mapper.XcUserMapper; import com.xuecheng.ucenter.model.po.XcUser; 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.Service; /** * @author Mr.M * @version 1.0 * @description TODO * @date 2022/9/28 18:09 */ @Service public class UserServiceImpl implements UserDetailsService { @Autowired XcUserMapper xcUserMapper; /** * @description 根据账号查询用户信息 * @param s 账号 * @return org.springframework.security.core.userdetails.UserDetails * @author Mr.M * @date 2022/9/28 18:30 */ @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { XcUser user = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>().eq(XcUser::getUsername, s)); if(user==null){ //返回空表示用户不存在 return null; } //取出数据库存储的正确密码 String password =user.getPassword(); //用户权限,如果不加报Cannot pass a null GrantedAuthority collection String[] authorities= {"p1"}; //为了安全令牌中不妨密码 user.setPassword(null); //将user对象装换成json String userString = JSON.toJSONString(user); //创建UserDetails对象,权限信息待实现授权功能时再向UserDetail中加入 我们只需要将数据库中查询的密码封装进去,springscruty会自动帮我们进行校验密码是否正确 UserDetails userDetails = User.withUsername(userString).password(password).authorities(authorities).build(); return userDetails; } }
第二个要提到的就是我们要扩展userDetails中信息
我们可以看到我们的UserDetail中好像只有UserName Password信息,实际上它的属性信息确实很少,有什么呢看下图。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
可以看到他有这些信息,注意这个类不是我们自己定义的,这是springScruty内部使用的类,我们自这个上面进行扩展太麻烦了,所以提出来直接在withUsername(userString)中传入一个我们从数据库中查询的对象json。然后我们的微服务就可以获取json然后将json转化成一个对象。
微服务中的获取代码如下(我是使用工具类封装的功能)
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; /** * @author Mr.M * @version 1.0 * @description 获取当前用户身份工具类 * @date 2022/10/18 18:02 */ @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; } }
下面是我接口中使用工具类进行实现的代码
@GetMapping("/course/{courseId}") @ApiOperation("根据课程id查询课程信息") 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; }
这样我们就能轻而易举的拿到我们令牌中的存储的登录信息。