1. 什么是微服务
1.1 微服务由来
微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
1.2 微服务优势
(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
1.3 微服务本质
(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。
2. 微服务认证与授权实现思路
2.1 认证授权过程分析
(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。(单点登录(SSO)用session并不是很方便,因为微服务有众多的模块)
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去。(按照一定的规则生成的字符串,字符串里面带着用户的身份信息)
2.2 权限管理数据模型
Gateway:将项目的端口号反向代理到其他端口统一暴露给用户使用;
Nacos注册中心:
2.3 jwt 介绍
2.3.1 访问令牌的类型
2.3.2 JWT 的组成
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名
JWT 头
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。
{
“alg”: “HS256”,
“typ”: “JWT”
}
在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);
typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述
JSON 对象转换为字符串保存。
有效载荷
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT
指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
{
“sub”: “1234567890”,
“name”: “Helen”,
“admin”: true
}
请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息
字段,存放保密信息,以防止信息泄露。
JSON 对象也使用 Base64 URL 算法转换为字符串保存。
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡
改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公
开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成
签名。
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个
部分用"."分隔,就构成整个 JWT 对象。
Base64URL 算法
如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算
法类似,稍有差别。
作为令牌的 JWT 可以放在 URL 中(例如 api.example/?token=xxx)。 Base64 中用的三个
字符是"+","/“和”=",由于在 URL 中有特殊含义,因此 Base64URL 中对他们做了替换:
"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是 Base64URL 算法。
3. 核心代码
DefaultPasswordEncoder.java
/**
* 密码处理类
*/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
public DefaultPasswordEncoder() {
this(-1);
}
public DefaultPasswordEncoder(int strength) {
}
//进行MD5加密
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
//进行密码比对
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
TokenLogoutHandler.java
/**
* 退出对token进行处理类
*/
public class TokenLogoutHandler implements LogoutHandler {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
//从请求头中获取token
String token = request.getHeader("token");
//判断是否为空,不为空则移除,从redis删除token
if (token != null) {
tokenManager.removeToken(token);
//从token中获取用户名
String username = tokenManager.getUserInfoFromToken(token);
redisTemplate.delete(username);
}
ResponseUtil.out(response, R.ok());
}
}
TokenManager.java
/**
* token处理类
*/
@Component
public class TokenManager {
//token有效时长
private long tokenEcpiration = 24*60*60*1000;
//编码秘钥
private String tokenSignKey = "123456";
//1 使用jwt根据用户名生成token
public String createToken(String username) {
String token = Jwts.builder().setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenEcpiration))
.signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
return token;
}
//2 根据token字符串得到用户信息
public String getUserInfoFromToken(String token) {
String userinfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
return userinfo;
}
//3 删除token
public void removeToken(String token) { }
}
UnauthEntryPoint.java
/**
* 未授权的处理了类
*/
public class UnauthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
TokenAuthFilter.java
/**
* 自定义授权过滤器
*/
public class TokenAuthFilter extends BasicAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
public TokenAuthFilter(AuthenticationManager authenticationManager,TokenManager tokenManager,RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//获取当前认证成功用户权限信息
UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);
//判断如果有权限信息,放到权限上下文中
if(authRequest != null) {
SecurityContextHolder.getContext().setAuthentication(authRequest);
}
chain.doFilter(request,response);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
//从header获取token
String token = request.getHeader("token");
if(token != null) {
//从token获取用户名
String username = tokenManager.getUserInfoFromToken(token);
//从redis获取对应权限列表
List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);
Collection<GrantedAuthority> authority = new ArrayList<>();
for(String permissionValue : permissionValueList) {
SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
authority.add(auth);
}
return new UsernamePasswordAuthenticationToken(username,token,authority);
}
return null;
}
}
TokenLoginFilter.java
/**
* 认证自定义过滤器
*/
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private AuthenticationManager authenticationManager;
public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);//设置是否只为post请求
//设置登录的路径,且请求方式为post
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
}
//1 获取表单提交用户名和密码
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
User user = new ObjectMapper().readValue(request.getInputStream(), User.class);
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(),new ArrayList<>()));
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException();
}
}
//2 认证成功调用的方法
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//认证成功,得到认证成功之后用户信息
SecurityUser user = (SecurityUser)authResult.getPrincipal();
//根据用户名生成token
String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
//把用户名称和用户权限列表放到redis
redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());
//返回token
ResponseUtil.out(response, R.ok().data("token",token));
}
//3 认证失败调用的方法
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
ResponseUtil.out(response, R.error());
}
}
User.java
@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "微信openid")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
SecurityUser.java
@Data
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
TokenWebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
private TokenManager tokenManager;
private RedisTemplate redisTemplate;
private DefaultPasswordEncoder defaultPasswordEncoder;
private UserDetailsService userDetailsService;
@Autowired
public TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
TokenManager tokenManager, RedisTemplate redisTemplate) {
this.userDetailsService = userDetailsService;
this.defaultPasswordEncoder = defaultPasswordEncoder;
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
/**
* 配置设置
* @param http
* @throws Exception
*/
//设置退出的地址和token,redis操作地址
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.authenticationEntryPoint(new UnauthEntryPoint())//没有权限访问
.and().csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and().logout().logoutUrl("/admin/acl/index/logout")//退出路径
.addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()
.addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))
.addFilter(new TokenAuthFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
}
//调用userDetailsService和密码处理
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
}
//不进行认证的路径,可以直接访问
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/api/**");
}
}
3. 案例实现
https://gitee.com/Itdoudoulzx/springcloud_springsecurity.git
内容参考:https://www.bilibili.com/video/BV15a411A7kP
仅用于学习!