springcloud微服务体系(一)— 基于security和jwt实现认证及鉴权服务
文章目录
- 需求
- 知识点讲解
- 方案
- SpringSecurity
- 具体实现
- 业务流程
- 代码
- 认证服务
- 鉴权服务
- 配置
需求
1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式
知识点讲解
方案
传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息
但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。
目前主流的方案由四种
- 单点登录(SSO)
一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。
- 分布式 Session 方案
借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。
- 客户端 Token 方案
令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。
- 客户端 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实现认证及鉴权服务相关推荐
- 微服务架构下的安全认证与鉴权
微服务架构下的安全认证与鉴权 转载自:https://mp.weixin.qq.com/s/qBJ_257IWn3cctqmKfJ7FQ 作者:王海龙,来自:EAWorld 现任普元云计算架构师,毕业 ...
- 微服务接入oauth2_微服务权限终极解决方案,Spring Cloud Gateway+Oauth2实现统一认证和鉴权!...
最近发现了一个很好的微服务权限解决方案,可以通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权.此方案为目前最新方案,仅支持Spring Boot 2.2.0.Spring Cloud Hox ...
- Kong社区版集成Keycloak实现微服务认证与鉴权
文章目录 Kong社区版集成Keycloak实现微服务认证与鉴权 前言 认证和鉴权流程 在Keycloak上配置 创建Realm 创建Client 创建Role 创建User 服务 环境准备 受保护的 ...
- 「springcloud 2021 系列」Spring Cloud Gateway + OAuth2 + JWT 实现统一认证与鉴权
通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权. 将采用 Nacos 作为注册中心,Gateway 作为网关,使用 nimbus-jose-jwt JWT 库操作 JWT 令牌 理论介绍 ...
- Spring Security源码解析(一)——认证和鉴权
目录 认证过程 AuthenticationManager Authentication AbstractAuthenticationToken UsernamePasswordAuthenticat ...
- Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务(三):RSA(RS512) 签名 JWT(附demo)
系列 云原生 API 网关,gRPC-Gateway V2 初探 Go + gRPC-Gateway(V2) 构建微服务实战系列,小程序登录鉴权服务:第一篇 Go + gRPC-Gateway(V2) ...
- Spring全家桶-Spring Security之自定义数据库表认证和鉴权
Spring全家桶-Spring Security之自定义数据库表认证和鉴权 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供 ...
- Tornado做鉴权服务性能实践
一. Torado实现鉴权服务 使用python的第三方jwt做鉴权服务, 生产token代码: def create_token(self, userId, product, level):payl ...
- 小程序微信授权登录服务器异常,解决调试腾讯云微信小程序Demo错误“登录失败:调用鉴权服务失败#40029_WEIXIN_CODE_ERR”...
此文章解决大家有可能遇到的"登录失败:调用鉴权服务失败#40029的问题"~~ 很多人出现上面的问题,那是因为:如果在购买解决方案时,把AppId 和 AppSecret 填写错误 ...
最新文章
- 细品经典:LeNet-1, LeNet-4, LeNet-5, Boosted LeNet-4
- 刷新table数据_经典 - 一文轻松看懂数据透视表
- python socket练习
- 如何使用React Hook
- 录播软件开始麦克风应该打开还是关闭
- apple的photo实际上是一个dashboard
- 【leetcode】109. Convert Sorted List to Binary Search Tree
- Idea 依赖冲突一分钟解决2种方案
- 各执一词,民用安防市场现状看法PK
- POJ 3668 枚举?
- keepalived 二
- element-ui 下拉框实现拼音搜索
- 日照-公积金贷款逾期预测-比赛总结
- python自动化测试面试题代码_Python自动化测试面试题-编程篇
- FPGA基础设计(9)Verilog数据类型和表达式
- spoolsv解决方法
- 开源GIS技术讨论,欢迎加群
- 【电力电子】【2016.05】【含源码】三相四线制配电系统的电流不平衡校正
- Elasticsearch7.X 入门学习第九课笔记-----聚合分析Aggregation
- 4.1TSV文件的抽取
热门文章
- 民宿管理系统课程设计_基于jsp的民宿网站-JavaEE实现民宿网站 - java项目源码
- 计算机基础pdf脚本之家,使用脚本管理Windows网络(更新版).pdf
- 计算机网络技术可以纹身吗,不思议迷宫纹身师冈布奥角色介绍 纹身师技能天赋...
- innodb OSC
- 计算机专业过年回家,回家过年的温暖唯美句子 描写过年回家的优美句子
- 和平精英android怎么写符号,和平精英特殊符号怎么打_和平精英名字特殊符号设置方法_游戏吧...
- 奶茶店转型一20200510
- 复古风吹到科技圈,老玩意也有新意思
- 魔兽世界最新服务器推荐,《魔兽世界》全新第七大区服务器推荐
- Fabfilter发布虚拟合成器插件-Twin 3