五、SpringSecurity微服务架构下的方案
1、微服务的概述
- 微服务概述:
- 微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,这些服务可以使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。
- 微服务优势
- 微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
- 微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。
- 微服务每个模块都可以使用不同的开发技术,开发模式更灵活。
- 微服务本质
- 系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级。这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
- 微服务的目的是有效的拆分应用,实现敏捷开发和部署。
2、微服务认证与授权实现思路
-
认证授权过程分析
- 基于Token,解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去
- 思路流程:因为模块众多每个模块都需要进行授权与认证,所以我们选择基于 token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为 key,权限列表为 value 的形式存入 redis 缓存中,根据用户名相关信息生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前请求是否有权限访问
- 思路流程图:
-
详细代码流程(仅供参考不做解释)
-
编写核心配置类
-
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private UserDetailsService userDetailsService; private TokenManager tokenManager; private DefaultPasswordEncoder defaultPasswordEncoder; private RedisTemplate redisTemplate; //自动注入一下需要的依赖 @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 */ @Override protected void configure(HttpSecurity http) throws Exception { http.exceptionHandling() .authenticationEntryPoint(new UnauthorizedEntryPoint())//无权限处理 .and().csrf().disable()//关闭csrf .authorizeRequests() .anyRequest().authenticated()//任何请求都需要验证 .and().logout().logoutUrl("/admin/acl/index/logout")//登出的请求路径 .addLogoutHandler(new TokenLogoutHandler(tokenManager,redisTemplate)).and()//登出处理器 .addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))//重写的UsernamePasswordAuthenticationFilter接口实现类 .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();//过滤请求是否包含token } /** * 密码处理 * @param auth * @throws Exception */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder); } /** * 配置哪些请求不拦截 * @param web * @throws Exception */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**", "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**" ); } }
-
-
相关的工具配置类
-
//DefaultPasswordEncoder:密码处理的方法 @Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder() { this(-1); } /** * @param strength * the log rounds to use, between 4 and 31 */ public DefaultPasswordEncoder(int strength) { } public String encode(CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); } }
-
//TokenManager:token 操作的工具类 @Component public class TokenManager { private long tokenExpiration = 24*60*60*1000; private String tokenSignKey = "123456"; public String createToken(String username) { String token = Jwts.builder().setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact(); return token; } public String getUserFromToken(String token) { String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject(); return user; } public void removeToken(String token) { //jwttoken无需删除,客户端扔掉即可。 } }
-
//TokenLogoutHandler:退出实现 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) { String token = request.getHeader("token"); if (token != null) { tokenManager.removeToken(token); //清空当前用户缓存中的权限数据 String userName = tokenManager.getUserFromToken(token); redisTemplate.delete(userName); } ResponseUtil.out(response, R.ok()); } }
-
//UnauthorizedEntryPoint:未授权统一处理 public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
-
-
创建认证授权实体类
-
@Data @Slf4j 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; } }
-
package com.qzh.serurity.entity; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import java.io.Serializable; /** * <p> * 用户实体类 * </p> * * @author qy * @since 2019-11-08 */ @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; }
-
-
创建认证和授权的filter
-
//TokenLoginFilter:认证的 filter public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) { this.authenticationManager = authenticationManager; this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; this.setPostOnly(false); this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); } catch (IOException e) { throw new RuntimeException(e); } } /** * 登录成功 * @param req * @param res * @param chain * @param auth * @throws IOException * @throws ServletException */ @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { SecurityUser user = (SecurityUser) auth.getPrincipal(); String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList()); ResponseUtil.out(res, R.ok().data("token", token)); } /** * 登录失败 * @param request * @param response * @param e * @throws IOException * @throws ServletException */ @Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
-
//TokenAuthenticationFilter:授权 filter public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) { super(authManager); this.tokenManager = tokenManager; this.redisTemplate = redisTemplate; } @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.info("================="+req.getRequestURI()); if(req.getRequestURI().indexOf("admin") == -1) { chain.doFilter(req, res); return; } UsernamePasswordAuthenticationToken authentication = null; try { authentication = getAuthentication(req); } catch (Exception e) { ResponseUtil.out(res, R.error()); } if (authentication != null) { SecurityContextHolder.getContext().setAuthentication(authentication); } else { ResponseUtil.out(res, R.error()); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) { // token置于header里 String token = request.getHeader("token"); if (token != null && !"".equals(token.trim())) { String userName = tokenManager.getUserFromToken(token); List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName); Collection<GrantedAuthority> authorities = new ArrayList<>(); for(String permissionValue : permissionValueList) { if(StringUtils.isEmpty(permissionValue)) continue; SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue); authorities.add(authority); } if (!StringUtils.isEmpty(userName)) { return new UsernamePasswordAuthenticationToken(userName, token, authorities); } return null; } return null; } }
-
-
编写UserDetailsService接口实现类
-
//查询用户具体信息 @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserService userService; @Autowired private PermissionService permissionService; /*** * 根据账号获取用户信息 * @param username: * @return: org.springframework.security.core.userdetails.UserDetails */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 从数据库中取出用户信息 User user = userService.selectByUsername(username); // 判断用户是否存在 if (null == user){ //throw new UsernameNotFoundException("用户名不存在!"); } // 返回UserDetails实现类 com.qzh.serurity.entity.User curUser = new com.qzh.serurity.entity.User(); BeanUtils.copyProperties(user,curUser); //从数据库获取用户权限 List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId()); SecurityUser securityUser = new SecurityUser(curUser); securityUser.setPermissionValueList(authorities); return securityUser; } }
-
-