前段时间一直在研究微服务的认证和授权的方式,网上给了大致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);
}
}
最终可以微服务中拿到用户信息....
大概就修改了这么多,基本满足了自己微服务的一些要求,实际上这种认证授权并没有真正意义上的授权,在网关通过路径来对各个微服务进行拦截,可能会对增大网关的压力,有待后期考察.