文章目录

  • 需求
  • 知识点讲解
    • 方案
    • SpringSecurity
  • 具体实现
    • 业务流程
    • 代码
    • 认证服务
    • 鉴权服务
    • 配置

需求

1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式

知识点讲解

方案

传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息

但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。

目前主流的方案由四种

  1. 单点登录(SSO)

一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。

  1. 分布式 Session 方案

借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。

  1. 客户端 Token 方案

令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。

  1. 客户端 Token 与 API 网关结合

这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。

本文就采用方案4,实现微服务体系中用户鉴权及认证服务。

Token的实现方案业界有多套成熟的方案,这其中最主流的是JWT 和 Oauth2.0 两种方式。
下面就基于JWT的方式具体实现。

SpringSecurity

AuthenticationManager, 用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现。当然事情肯定不是它来做,具体校验动作会由AuthenticationManager将请求转发给具体的实现类来做。根据实现反馈的结果再调用具体的Handler来给用户以反馈。

AuthenticationProvider, 认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风,主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。
前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。

UserDetailService, 用户认证通过Provider来做,所以Provider需要拿到系统已经保存的认证信息,获取用户信息的接口spring-security抽象成UserDetailService。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。

AuthenticationToken, 所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken。

SecurityContext,当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的。

具体实现

业务流程

  • 客户端调用登录接口,传入用户名密码。
  • 服务端请求身份认证中心,确认用户名密码正确。
  • 服务端创建JWT,返回给客户端。
  • 客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie中)在后续请求中,在 HTTP 请求头中加上 JWT。
  • 服务端校验 JWT,校验通过后,返回相关资源和数据。

代码

完整pom文件(项目结构为多模块)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>springcloud</artifactId><groupId>com.lhm</groupId><version>1.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>security</artifactId><dependencies><!--web 服务--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--security--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency><!--mybatis-plus--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.0</version></dependency><!--mybatis-plus日志--><dependency><groupId>p6spy</groupId><artifactId>p6spy</artifactId><version>3.8.1</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- druid的starter --><dependency><groupId>com.alibaba</groupId><artifactId>druid-spring-boot-starter</artifactId><version>1.1.9</version></dependency><!-- redis --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--JSON--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.38</version></dependency><!-- StringUtils相关工具类jar包 --><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.4</version></dependency><!--  lombok  --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency></dependencies>
</project>

认证服务


在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现UserDetailsService开始

package com.lhm.springcloud.security.service.impl;import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.pojo.AuthUserPoJo;
import com.lhm.springcloud.security.service.IUsersService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;/*** @ClassName UserDetailsServiceImpl* @Description  实现security提供的 用户信息获取接口  并按照业务增加redis 登陆限制* @Author liuheming* @Date 2019/5/6 10:26* @Version 1.0**/
@Component
public class UserDetailsServiceImpl implements UserDetailsService {//登入重试时间@Value("${security.loginAfterTime}")private Integer loginAfterTime;@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IUsersService iUsersService;/*** @Author liuheming* @Description 实现用户信息查询方法 让DaoAuthenticationProvider 获取到数据库获中用户数据* @Date 11:21 2019/5/6* @Param [username]* @return org.springframework.security.core.userdetails.UserDetails**/@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {String flagKey = "loginFailFlag:"+username;String value = redisTemplate.opsForValue().get(flagKey);if(StringUtils.isNotBlank(value)){//超过限制次数throw new UsernameNotFoundException("登录错误次数超过限制,请"+loginAfterTime+"分钟后再试");}//查询用户信息AuthUserPoJo authUserPoJo=iUsersService.findAuthUserByUsername(username);if(null==authUserPoJo){throw new UsernameNotFoundException("当前用户不存在");}if(authUserPoJo.getRoleInfos()==null || authUserPoJo.getRoleInfos().isEmpty()){throw new UsernameNotFoundException("当前用户无角色");}return new AuthUserDetails(authUserPoJo);}
}

UserDetailsServiceImpl 最后返回一个拼装好的security用户对象,但为了实现自定义角色与权限管理需要对UserDetails进行重写。

package com.lhm.springcloud.security.pojo;import com.lhm.springcloud.security.constant.UserConstant;
import com.lhm.springcloud.security.entity.PermissionInfo;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.ArrayList;
import java.util.Collection;
import java.util.List;/*** @author Exrickx*/public class AuthUserDetails extends AuthUserPoJo implements UserDetails {private static final long serialVersionUID = 1L;public AuthUserDetails(AuthUserPoJo user) {if (user != null) {this.setUserName(user.getUserName());this.setPassWord(user.getPassWord());this.setStatus(user.getStatus());this.setRoleInfos(user.getRoleInfos());this.setPermissionInfos(user.getPermissionInfos());}}//将角色权限 放入GrantedAuthorit的自定义实现类MyGrantedAuthority中  为权限判定提供数据@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {List<GrantedAuthority> authorityList = new ArrayList<GrantedAuthority>();List<PermissionInfo> permissions = this.getPermissionInfos();if (permissions != null) {for (PermissionInfo permission : permissions) {GrantedAuthority grantedAuthority = new MyGrantedAuthority(permission.getPath(), permission.getMethod());authorityList.add(grantedAuthority);}}return authorityList;}@Overridepublic String getPassword() {return super.getPassWord();}@Overridepublic String getUsername() {return super.getUserName();}/*** 账户是否过期** @return*/@Overridepublic boolean isAccountNonExpired() {return true;}/*** 是否禁用** @return*/@Overridepublic boolean isAccountNonLocked() {return true;}/*** 密码是否过期** @return*/@Overridepublic boolean isCredentialsNonExpired() {return true;}/*** 是否启用** @return*/@Overridepublic boolean isEnabled() {return UserConstant.USER_STATUS_NORMAL.equals(this.getStatus()) ? true : false;}
}

然后DaoProvider会对比校验并执行相应的结果处理器

登入成功处理器

package com.lhm.springcloud.security.handler;import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.pojo.AuthUserDetails;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;/*** @ClassName LoginSuccessHandlerFilter* @Description 登陆认证成功处理过滤器* @Author liuheming* @Date 2019/5/6 16:27* @Version 1.0**/
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {@Autowiredprivate TokenUtil tokenUtil;/*** @Author liuheming* @Description 用户认证成功后 生成token并返回* @Date 8:50 2019/5/7* @Param [request, response, authentication]* @return void**/@Overridepublic void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {AuthUserDetails authUserDetails=(AuthUserDetails)authentication.getPrincipal();//从内存中获取当前认证用户信息//创建tokenString accessToken = tokenUtil.createAccessJwtToken(authUserDetails);String refreshToken = tokenUtil.createRefreshToken(authUserDetails);HashMap<String,String> map=new HashMap<>();map.put("accessToken",accessToken);map.put("refreshToken",refreshToken);ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.OK,"登录成功",map));}}

登入失败处理器

package com.lhm.springcloud.security.handler;import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;/*** @ClassName LoginFailureHandler* @Description 登陆失败处理过滤器* @Author liuheming* @Date 2019/5/7 9:05* @Version 1.0**/
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {//#限制用户登陆错误次数(次)@Value("${security.loginTimeLimit}")private Integer loginTimeLimit;//#错误超过次数后多少分钟后才能继续登录(分钟)@Value("${security.loginAfterTime}")private Integer loginAfterTime;@Autowiredprivate StringRedisTemplate redisTemplate;/*** @Author liuheming* @Description 用户登陆失败处理类  记录用户登陆错误次数* @Date 9:12 2019/5/7* @Param [request, response, e]* @return void**/@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {String username = request.getParameter("username");recordLoginTime(username);String key = "loginTimeLimit:" + username;String value = redisTemplate.opsForValue().get(key);if (StringUtils.isBlank(value)) {value = "0";}//获取已登录错误次数int loginFailTime = Integer.parseInt(value);int restLoginTime = loginTimeLimit - loginFailTime;ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "用户名或密码错误"));} else if (e instanceof DisabledException) {ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "账户被禁用,请联系管理员"));} else {ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, "登录失败"));}}/*** 判断用户登陆错误次数*/public boolean recordLoginTime(String username) {String key = "loginTimeLimit:" + username;String flagKey = "loginFailFlag:" + username;String value = redisTemplate.opsForValue().get(key);if (StringUtils.isBlank(value)) {value = "0";}//获取已登录错误次数int loginFailTime = Integer.parseInt(value) + 1;redisTemplate.opsForValue().set(key, String.valueOf(loginFailTime), loginAfterTime, TimeUnit.MINUTES);if (loginFailTime >= loginTimeLimit) {redisTemplate.opsForValue().set(flagKey, "fail", loginAfterTime, TimeUnit.MINUTES);return false;}return true;}
}

在登入的过程中会对用户的请求间隔时间及失败次数做记录。

鉴权服务

鉴权的过程分成了两个大的步骤
第一对请求的路径、方法、头部信息进行判断,确认该请求是否需要鉴权
JWTAuthenticationFilter

package com.lhm.springcloud.security.filter;import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.constant.ResultCode;
import com.lhm.springcloud.security.constant.SecurityConstant;
import com.lhm.springcloud.security.exception.CommonException;
import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import com.lhm.springcloud.security.utils.ResUtil;
import com.lhm.springcloud.security.utils.ResponseUtil;
import com.lhm.springcloud.security.utils.SpringUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;/*** JWT过滤器1*/public class JWTAuthenticationFilter extends BasicAuthenticationFilter {public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {super(authenticationManager);}public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {super(authenticationManager, authenticationEntryPoint);}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {IgnoredUrlsProperties ignoredUrlsProperties= SpringUtil.getBean("ignoredUrlsProperties", IgnoredUrlsProperties.class);String Requesturl=request.getRequestURI();PathMatcher pathMatcher = new AntPathMatcher();if(null != ignoredUrlsProperties){for(String url:ignoredUrlsProperties.getUrls()){if(pathMatcher.match(url,Requesturl)){chain.doFilter(request, response);return;}}}//获取请求头String header = request.getHeader(SecurityConstant.HEADER);//如果请求头中不存在 或  格式不对  则进入下个过滤器if (StringUtils.isBlank(header) || !header.startsWith(SecurityConstant.TOKEN_SPLIT)) {chain.doFilter(request, response);return;}try {UsernamePasswordAuthenticationToken authentication = getAuthentication(request, response);SecurityContextHolder.getContext().setAuthentication(authentication);} catch (Exception e) {ResponseUtil.out(response, ResUtil.getJsonStr(ResultCode.BAD_REQUEST, e.getMessage()));return;}chain.doFilter(request, response);}/*** @Author liuheming* @Description 对token进行解析认证* @Date 11:11 2019/5/7* @Param [request, response]* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken**/private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response) throws CommonException {String token = request.getHeader(SecurityConstant.HEADER);if (StringUtils.isNotBlank(token)) {// 解析tokenClaims claims = null;try {claims = Jwts.parser().setSigningKey(SecurityConstant.tokenSigningKey).parseClaimsJws(token.replace(SecurityConstant.TOKEN_SPLIT, "")).getBody();//获取用户名String username = claims.getSubject();//获取权限List<MyGrantedAuthority> authorities = new ArrayList<MyGrantedAuthority>();String authority = claims.get(SecurityConstant.AUTHORITIES).toString();if (StringUtils.isNotBlank(authority)) {JSONArray list=JSONArray.parseArray(authority);for (int i=0;i<list.size();i++){JSONObject jsonObject=list.getJSONObject(i);authorities.add(new MyGrantedAuthority(jsonObject.getString("path"),jsonObject.getString("method")));}}if (StringUtils.isNotBlank(username)) {//此处password不能为nullUser principal = new User(username, "", authorities);return new UsernamePasswordAuthenticationToken(principal, null, authorities);}} catch (ExpiredJwtException e) {throw new CommonException(ResultCode.BAD_REQUEST, "登录已失效,请重新登录");} catch (Exception e) {throw new CommonException(ResultCode.BAD_REQUEST, "解析token错误");}}return null;}}

第二判断当前请求token是否有权访问当前请求地址
MyFilterSecurityInterceptor

package com.lhm.springcloud.security.filter;import com.lhm.springcloud.security.manager.MyAccessDecisionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.SecurityMetadataSource;
import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
import org.springframework.security.access.intercept.InterceptorStatusToken;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;import javax.servlet.*;
import java.io.IOException;/*** 权限管理过滤器2* 监控用户行为* @author Exrickx*/@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {@Autowiredprivate FilterInvocationSecurityMetadataSource securityMetadataSource;@Autowiredpublic void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {super.setAccessDecisionManager(myAccessDecisionManager);}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);invoke(fi);}//fi里面有一个被拦截的url//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够public void invoke(FilterInvocation fi) throws IOException, ServletException {InterceptorStatusToken token = super.beforeInvocation(fi);try {fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} finally {super.afterInvocation(token, null);}}@Overridepublic void destroy() {}@Overridepublic Class<?> getSecureObjectClass() {return FilterInvocation.class;}@Overridepublic SecurityMetadataSource obtainSecurityMetadataSource() {return this.securityMetadataSource;}
}

具体的处理会放到MySecurityMetadataSource中去判断,不过我这里做了个小优化,将处理权限的业务统一放到了MyAccessDecisionManager下,减少点性能开销

MySecurityMetadataSource

package com.lhm.springcloud.security.manager;import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;import java.util.ArrayList;
import java.util.Collection;/*** 权限资源管理器* 为权限决断器提供支持** @author Exrickx*/@Component
public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource {/*** 此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。* 因为每一次来了请求,都先要匹配一下权限表中的信息是不是包含此url,* 因此优化一下,对url直接拦截,不管请求的url 是什么都直接拦截,然后在MyAccessDecisionManager的decide 方法中做拦截还是放行的决策。* 所以此方法的返回值不能返回 null 此处随便返回一下。** @param o* @return* @throws IllegalArgumentException*/@Overridepublic Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {Collection<ConfigAttribute> co = new ArrayList<>();co.add(new SecurityConfig("null"));return co;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> aClass) {return true;}
}

MyAccessDecisionManager

package com.lhm.springcloud.security.manager;import com.lhm.springcloud.security.pojo.MyGrantedAuthority;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Service;import javax.servlet.http.HttpServletRequest;
import java.util.Collection;/*** @ClassName MyAccessDecisionManager* @Description 权限最终判断器*  * 判断用户拥有的角色是否有资源访问权限* @Author liuheming* @Date 2019/5/7 10:44* @Version 1.0**/
@Service
public class MyAccessDecisionManager implements AccessDecisionManager {//decide 方法是判定是否拥有权限的决策方法@Overridepublic void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();String url, method;AntPathRequestMatcher matcher;for (GrantedAuthority ga : authentication.getAuthorities()) {if (ga instanceof MyGrantedAuthority) {MyGrantedAuthority urlGrantedAuthority = (MyGrantedAuthority) ga;url = urlGrantedAuthority.getPermissionUrl();method = urlGrantedAuthority.getMethod();matcher = new AntPathRequestMatcher(url);if (matcher.matches(request)) {//当权限表权限的method为ALL时表示拥有此路径的所有请求方式权利。if (method.equals(request.getMethod()) || "ALL".equals(method)) {return;}}}throw new AccessDeniedException("您没有访问权限");}throw new AccessDeniedException("鉴权出错");}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}

decide()方法中的MyGrantedAuthority是我自定义的权限对象 因为原有的SimpleGrantedAuthority类只有一个属性,无法完成RESTfull风格的请求。
MyGrantedAuthority

package com.lhm.springcloud.security.pojo;import org.springframework.security.core.GrantedAuthority;/*** @ClassName MyGrantedAuthority* @Description TODO* @Author liuheming* @Date 2019/5/7 10:39* @Version 1.0**/
public class MyGrantedAuthority implements GrantedAuthority {private String url;private String method;public String getPermissionUrl() {return url;}public void setPermissionUrl(String permissionUrl) {this.url = permissionUrl;}public String getMethod() {return method;}public void setMethod(String method) {this.method = method;}public MyGrantedAuthority(String url, String method) {this.url = url;this.method = method;}@Overridepublic String getAuthority() {return this.url + ";" + this.method;}
}

配置

最后将我们自定义的类全部注入到security提供的配置文件类中,具体的配置我都用注解表明了。

package com.lhm.springcloud.security.config;import com.lhm.springcloud.security.constant.IgnoredUrlsProperties;
import com.lhm.springcloud.security.filter.JWTAuthenticationFilter;
import com.lhm.springcloud.security.filter.MyFilterSecurityInterceptor;
import com.lhm.springcloud.security.filter.WebSecurityCorsFilter;
import com.lhm.springcloud.security.handler.RestAccessDeniedHandler;
import com.lhm.springcloud.security.service.impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;/** Security 核心配置类* 开启控制权限至Controller* @author Exrickx* */@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {@Autowiredprivate IgnoredUrlsProperties ignoredUrlsProperties;@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Autowiredprivate AuthenticationSuccessHandler successHandler;@Autowiredprivate AuthenticationFailureHandler failHandler;@Autowiredprivate RestAccessDeniedHandler accessDeniedHandler;@Autowiredprivate MyFilterSecurityInterceptor myFilterSecurityInterceptor;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());//密码加密使用 Spring Security 提供的BCryptPasswordEncoder.encode(user.getRawPassword().trim())}@Overrideprotected void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();//除配置文件忽略路径其它所有请求都需经过认证和授权for(String url:ignoredUrlsProperties.getUrls()){registry.antMatchers(url).permitAll();}registry.antMatchers(HttpMethod.OPTIONS).permitAll().and()//表单登录方式.formLogin().loginPage("/login/needLogin")//登录需要经过的url请求.loginProcessingUrl("/api/v1/auth/login").usernameParameter("username").passwordParameter("password").permitAll()//成功处理类.successHandler(successHandler)//失败.failureHandler(failHandler).and().logout().permitAll().and().authorizeRequests()//任何请求.anyRequest()//需要身份认证.authenticated().and()//关闭跨站请求防护.csrf().disable()//前后端分离采用JWT 不需要session.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()//自定义权限拒绝处理类.exceptionHandling().accessDeniedHandler(accessDeniedHandler).and()//添加自定义权限过滤器.addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class).addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)//添加JWT过滤器 除/login其它请求都需经过此过滤器.addFilter(new JWTAuthenticationFilter(authenticationManager()));}}

以上就是最核心的代码部分,完整代码已经贴出,有兴趣的同学可以结合代码学习一下。
git:https://github.com/liuheming/springcloudDemo.git

springcloud微服务体系(一)— 基于security和jwt实现认证及鉴权服务相关推荐

  1. 微服务架构下的安全认证与鉴权

    微服务架构下的安全认证与鉴权 转载自:https://mp.weixin.qq.com/s/qBJ_257IWn3cctqmKfJ7FQ 作者:王海龙,来自:EAWorld 现任普元云计算架构师,毕业 ...

  2. 微服务接入oauth2_微服务权限终极解决方案,Spring Cloud Gateway+Oauth2实现统一认证和鉴权!...

    最近发现了一个很好的微服务权限解决方案,可以通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权.此方案为目前最新方案,仅支持Spring Boot 2.2.0.Spring Cloud Hox ...

  3. Kong社区版集成Keycloak实现微服务认证与鉴权

    文章目录 Kong社区版集成Keycloak实现微服务认证与鉴权 前言 认证和鉴权流程 在Keycloak上配置 创建Realm 创建Client 创建Role 创建User 服务 环境准备 受保护的 ...

  4. 「springcloud 2021 系列」Spring Cloud Gateway + OAuth2 + JWT 实现统一认证与鉴权

    通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权. 将采用 Nacos 作为注册中心,Gateway 作为网关,使用 nimbus-jose-jwt JWT 库操作 JWT 令牌 理论介绍 ...

  5. Spring Security源码解析(一)——认证和鉴权

    目录 认证过程 AuthenticationManager Authentication AbstractAuthenticationToken UsernamePasswordAuthenticat ...

  6. Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务(三):RSA(RS512) 签名 JWT(附demo)

    系列 云原生 API 网关,gRPC-Gateway V2 初探 Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇 Go + gRPC-Gateway(V2) ...

  7. Spring全家桶-Spring Security之自定义数据库表认证和鉴权

    Spring全家桶-Spring Security之自定义数据库表认证和鉴权 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供 ...

  8. Tornado做鉴权服务性能实践

    一. Torado实现鉴权服务 使用python的第三方jwt做鉴权服务, 生产token代码: def create_token(self, userId, product, level):payl ...

  9. 小程序微信授权登录服务器异常,解决调试腾讯云微信小程序Demo错误“登录失败:调用鉴权服务失败#40029_WEIXIN_CODE_ERR”...

    此文章解决大家有可能遇到的"登录失败:调用鉴权服务失败#40029的问题"~~ 很多人出现上面的问题,那是因为:如果在购买解决方案时,把AppId 和 AppSecret 填写错误 ...

最新文章

  1. 细品经典:LeNet-1, LeNet-4, LeNet-5, Boosted LeNet-4
  2. 刷新table数据_经典 - 一文轻松看懂数据透视表
  3. python socket练习
  4. 如何使用React Hook
  5. 录播软件开始麦克风应该打开还是关闭
  6. apple的photo实际上是一个dashboard
  7. 【leetcode】109. Convert Sorted List to Binary Search Tree
  8. Idea 依赖冲突一分钟解决2种方案
  9. 各执一词,民用安防市场现状看法PK
  10. POJ 3668 枚举?
  11. keepalived 二
  12. element-ui 下拉框实现拼音搜索
  13. 日照-公积金贷款逾期预测-比赛总结
  14. python自动化测试面试题代码_Python自动化测试面试题-编程篇
  15. FPGA基础设计(9)Verilog数据类型和表达式
  16. spoolsv解决方法
  17. 开源GIS技术讨论,欢迎加群
  18. 【电力电子】【2016.05】【含源码】三相四线制配电系统的电流不平衡校正
  19. Elasticsearch7.X 入门学习第九课笔记-----聚合分析Aggregation
  20. 4.1TSV文件的抽取

热门文章

  1. 民宿管理系统课程设计_基于jsp的民宿网站-JavaEE实现民宿网站 - java项目源码
  2. 计算机基础pdf脚本之家,使用脚本管理Windows网络(更新版).pdf
  3. 计算机网络技术可以纹身吗,不思议迷宫纹身师冈布奥角色介绍 纹身师技能天赋...
  4. innodb OSC
  5. 计算机专业过年回家,回家过年的温暖唯美句子 描写过年回家的优美句子
  6. 和平精英android怎么写符号,和平精英特殊符号怎么打_和平精英名字特殊符号设置方法_游戏吧...
  7. 奶茶店转型一20200510
  8. 复古风吹到科技圈,老玩意也有新意思
  9. 魔兽世界最新服务器推荐,《魔兽世界》全新第七大区服务器推荐
  10. Fabfilter发布虚拟合成器插件-Twin 3