springcloud+gateway(zuul)+springsecurity+jwt实现简单的认证授权
前段时间一直在研究微服务的认证和授权的方式,网上给了大致4种模式,感觉配置起来都不是很得心应手,偶然间看到了一个简单且较为完整的jwt+springsecurity的配置方式,这里先给出参考的github上的源码:
https://github.com/shuaicj/zuul-auth-example
但跟着配置后,问题还是很多,套用到自己的微服务框架上还是有些难度.
github工程包里有4个核心的类
common下的
认证中心和网关下各有一个security的配置类
简单解释下这些类都是干什么的,就不粘代码了,github上有....
这个是auth下的securityconfig
gateway下的securityconfig
这里总结下修改的不同点,以及踩的坑:
1.auth下的securityconfig采用的内存存储模式
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); auth.inMemoryAuthentication() .withUser("admin").password(encoder.encode("admin")).roles("ADMIN", "USER").and() .withUser("shuaicj").password(encoder.encode("shuaicj")).roles("USER"); }
所以我们应该修改为数据库读取的模式
service层的这个loadUserByUsername用于根据登录时输入的账号来获取用户并得到权限
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) { if(StringUtils.isEmpty(username)) { throw new UsernameNotFoundException("UserDetailsService没有接收到用户账号"); } else { Example example = new Example(User.class); Example.Criteria criteria = example.createCriteria(); criteria.andEqualTo("username",username); List<User> users = userMapper.selectByExample(example); if(users == null) { throw new UsernameNotFoundException(String.format("用户'%s'不存在", username)); } User user = users.get(0); List<Role> roles = userMapper.findRoles(user.getId()); List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); for (Role role : roles) { //封装用户信息和角色信息到SecurityContextHolder全局缓存中 grantedAuthorities.add(new SimpleGrantedAuthority(role.getRole())); System.out.println(role); } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); } } }
这里就将原来的内存存储的机制修改为数据库后的方式
@Autowired protected void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { /** * 指定用户认证时,默认从哪里获取认证用户信息 */ auth.userDetailsService(userDetailsServiceImpl).passwordEncoder(new BCryptPasswordEncoder()); }
然后我们调用登录接口,这个路径在yml下配置....
然后把header中的token,拿去调用服务下的接口
然后就爆炸了....因为我数据库里这个用户是有权限的...但还是被禁止了...
折腾了一上午,终于跳出了坑,
这是正确的角色表存储方法
之前我一直是USER,ADMIN...因为在springsecurity下它的授权都是以ROLE_XXXX,而之前读取的是XXX,不匹配就禁止了,当然不想改数据库,可以在分配权限的地方都加上ROLE_,一样的效果.
2.获取登录获取token的方式,感觉不是很优雅,之前做项目都是有一个result返回类的,他这个登录后直接在header里加,感觉不是很习惯...
修改了下这个成功认证的方法,写了一个controller自定义了下返回集,把token带过去
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse rsp, FilterChain chain,
Authentication auth) throws IOException {
Instant now = Instant.now();
String token = Jwts.builder()
.setSubject(auth.getName())
.claim("authorities", auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()))
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plusSeconds(config.getExpiration())))
.signWith(SignatureAlgorithm.HS256, config.getSecret().getBytes())
.compact();
rsp.sendRedirect("/authSuccess?token="+config.getPrefix() + " " + token);
}
在用postman测试就有了自己想要的返回结果
3.模拟验证码,github工程里的登录是没有验证码的...想加入自己的验证方式,模拟下
这里同理重定向转发到自定义的返回集...然后在中间模拟checkCode
@Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse rsp) throws AuthenticationException, IOException { User u = null; try{ u = mapper.readValue(req.getInputStream(),User.class); if (!u.getCheck().equals("success")) { rsp.sendRedirect("/authError?message=checkCodeError"); } else { return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken( u.getUsername(), u.getPassword(), Collections.emptyList() )); } }catch (UnrecognizedPropertyException e) { e.printStackTrace(); System.out.println(e); rsp.sendRedirect("/authError?message=authParamsError"); } return null; }
这个就是重定向的controller,根据自己的需求更改返回集
@RestController public class AuthController { @RequestMapping("/authSuccess") public Result authSuccess(@PathParam("token") String token) { return Result.ok(token); } @RequestMapping("/authError") public Result authError(@PathParam("message") String message) { return Result.error(401,message); } }
4.在微服务zuul其他的service中请求头消失
这是我一个微服务想解析zuul转发的header中的token,拿到用户名和角色..然后就一直为空.........
@DeleteMapping("/admin") public Result admin(HttpServletRequest request) { request.getHeaderNames(); String username = JwtUtil.getUsername(request); List<String> authorities = JwtUtil.getAuthorities(request); System.out.println(username); for(String s : authorities) { System.out.println(s); } return Result.ok("admin"); }
找了网上的一些说法
说在网关的yml配置下这个就ok了,然并卵..
网上有人说配置下这个:
RequestContext context = RequestContext.getCurrentContext(); context.addZuulRequestHeader("Authorization",req.getHeader(config.getHeader()));
于是我便想到了在jwttoken认证过滤类中添加
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtAuthenticationConfig config;
public JwtTokenAuthenticationFilter(JwtAuthenticationConfig config) {
this.config = config;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse rsp, FilterChain filterChain)
throws ServletException, IOException {
String token = req.getHeader(config.getHeader());
if (token != null && token.startsWith(config.getPrefix() + " ")) {
token = token.replace(config.getPrefix() + " ", "");
try {
Claims claims = Jwts.parser()
.setSigningKey(config.getSecret().getBytes())
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
@SuppressWarnings("unchecked")
List<String> authorities = claims.get("authorities", List.class);
if (username != null) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null,
authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
SecurityContextHolder.getContext().setAuthentication(auth);
RequestContext context = RequestContext.getCurrentContext();
context.addZuulRequestHeader("Authorization",req.getHeader(config.getHeader()));
}
} catch (Exception ignore) {
SecurityContextHolder.clearContext();
}
}
filterChain.doFilter(req, rsp);
}
}
最终可以微服务中拿到用户信息....
大概就修改了这么多,基本满足了自己微服务的一些要求,实际上这种认证授权并没有真正意义上的授权,在网关通过路径来对各个微服务进行拦截,可能会对增大网关的压力,有待后期考察.