前段时间一直在研究微服务的认证和授权的方式,网上给了大致4种模式,感觉配置起来都不是很得心应手,偶然间看到了一个简单且较为完整的jwt+springsecurity的配置方式,这里先给出参考的github上的源码:
但跟着配置后,问题还是很多,套用到自己的微服务框架上还是有些难度.
github工程包里有4个核心的类
common下的
认证中心和网关下各有一个security的配置类
简单解释下这些类都是干什么的,就不粘代码了,github上有....
这个是auth下的securityconfig
gateway下的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); Listusers = userMapper.selectByExample(example); if(users == null) { throw new UsernameNotFoundException(String.format("用户'%s'不存在", username)); } User user = users.get(0); List roles = userMapper.findRoles(user.getId()); List 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_,一样的效果.
修改了下这个成功认证的方法,写了一个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测试就有了自己想要的返回结果
这里同理重定向转发到自定义的返回集...然后在中间模拟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); Listauthorities = 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") Listauthorities = 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); } }
最终可以微服务中拿到用户信息....
大概就修改了这么多,基本满足了自己微服务的一些要求,实际上这种认证授权并没有真正意义上的授权,在网关通过路径来对各个微服务进行拦截,可能会对增大网关的压力,有待后期考察.