一、需求
1、RESTfull风格的鉴权服务(路线相同的情况下根据请求方式鉴别访问权限)
2、包含用户、角色、权限
3、使用JWT最为token认证方式
二、知识点讲解 2.1 方案 传统的单体应用体系下,应用是一个整体,一般针对所有的请求都会进行权限校验。请求一般会通过一个权限的拦截器进行权限的校验,在登录时将用户信息缓存到 session 中,后续访问则从缓存中获取用户信息
但在微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。因此在设计架构中,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。
目前主流的方案由四种
2.1.1 单点登录(SSO) 一次登入,多地使用。这种方案意味着每个面向用户的服务都必须与认证服务交互,进而产生大量琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显。
2.1.2 分布式 Session 方案 借助reids或其他共享存储中,将用户认证的信息存储在其中,通常使用用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。
2.1.3 客户端 Token 方案 令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。对于客户端令牌的编码方案,Borsos 更喜欢使用 JSON Web Tokens(JWT),它足够简单且库支持程度也比较好。
2.1.4 客户端 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()到达同样的目的。
三、具体实现 3.1 业务流程
客户端调用登录接口,传入用户名密码。
服务端请求身份认证中心,确认用户名密码正确。
服务端创建JWT,返回给客户端。
客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie中)在后续请求中,在 HTTP 请求头中加上 JWT。
服务端校验 JWT,校验通过后,返回相关资源和数据。3.2 代码 完整pom文件(项目结构为多模块)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 <?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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <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 > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.1.0</version > </dependency > <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 > <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.1.9</version > </dependency > <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 > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.38</version > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > <version > 3.4</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <scope > provided</scope > </dependency > </dependencies > </project >
3.3 认证服务 在登入方面,本次使用了security默认提供的表单登陆方式,因此直接从实现 UserDetailsService开始
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 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;@Component public class UserDetailsServiceImpl implements UserDetailsService { @Value ("${security.loginAfterTime}" ) private Integer loginAfterTime; @Autowired private StringRedisTemplate redisTemplate; @Autowired private IUsersService iUsersService; @Override public 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进行重写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 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;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()); } } @Override public 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; } @Override public String getPassword () { return super .getPassWord(); } @Override public String getUsername () { return super .getUserName(); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return UserConstant.USER_STATUS_NORMAL.equals(this .getStatus()) ? true : false ; } }
然后DaoProvider会对比校验并执行相应的结果处理器
登入成功处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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;@Component public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Autowired private TokenUtil tokenUtil; @Override public void onAuthenticationSuccess (HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { AuthUserDetails authUserDetails=(AuthUserDetails)authentication.getPrincipal(); String 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)); } }
登入失败处理器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 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;@Component public class LoginFailureHandler implements AuthenticationFailureHandler { @Value ("${security.loginTimeLimit}" ) private Integer loginTimeLimit; @Value ("${security.loginAfterTime}" ) private Integer loginAfterTime; @Autowired private StringRedisTemplate redisTemplate; @Override public 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 ; } }
在登入的过程中会对用户的请求间隔时间及失败次数做记录。
3.4 鉴权服务 鉴权的过程分成了两个大的步骤
第一对请求的路径、方法、头部信息进行判断,确认该请求是否需要鉴权 JWTAuthenticationFilter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 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;public class JWTAuthenticationFilter extends BasicAuthenticationFilter { public JWTAuthenticationFilter (AuthenticationManager authenticationManager) { super (authenticationManager); } public JWTAuthenticationFilter (AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) { super (authenticationManager, authenticationEntryPoint); } @Override protected 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); } private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request, HttpServletResponse response) throws CommonException { String token = request.getHeader(SecurityConstant.HEADER); if (StringUtils.isNotBlank(token)) { Claims 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)) { User 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 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;@Component public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Autowired private FilterInvocationSecurityMetadataSource securityMetadataSource; @Autowired public void setMyAccessDecisionManager (MyAccessDecisionManager myAccessDecisionManager) { super .setAccessDecisionManager(myAccessDecisionManager); } @Override public void init (FilterConfig filterConfig) throws ServletException { } @Override public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } 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 ); } } @Override public void destroy () { } @Override public Class<?> getSecureObjectClass() { return FilterInvocation.class ; } @Override public SecurityMetadataSource obtainSecurityMetadataSource () { return this .securityMetadataSource; } }
具体的处理会放到MySecurityMetadataSource中去判断,不过我这里做了个小优化,将处理权限的业务统一放到了MyAccessDecisionManager下,减少点性能开销
MySecurityMetadataSource
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 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;@Component public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Override public Collection<ConfigAttribute> getAttributes (Object o) throws IllegalArgumentException { Collection<ConfigAttribute> co = new ArrayList<>(); co.add(new SecurityConfig("null" )); return co; } @Override public Collection<ConfigAttribute> getAllConfigAttributes () { return null ; } @Override public boolean supports (Class<?> aClass) { return true ; } }
MyAccessDecisionManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 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;@Service public class MyAccessDecisionManager implements AccessDecisionManager { @Override public 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)) { if (method.equals(request.getMethod()) || "ALL" .equals(method)) { return ; } } } throw new AccessDeniedException("您没有访问权限" ); } throw new AccessDeniedException("鉴权出错" ); } @Override public boolean supports (ConfigAttribute attribute) { return true ; } @Override public boolean supports (Class<?> clazz) { return true ; } }
decide()方法中的MyGrantedAuthority是我自定义的权限对象 因为原有的SimpleGrantedAuthority类只有一个属性,无法完成RESTfull风格的请求。 MyGrantedAuthority
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.lhm.springcloud.security.pojo;import org.springframework.security.core.GrantedAuthority;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; } @Override public String getAuthority () { return this .url + ";" + this .method; } }
配置 最后将我们自定义的类全部注入到security提供的配置文件类中,具体的配置我都用注解表明了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 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;@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IgnoredUrlsProperties ignoredUrlsProperties; @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failHandler; @Autowired private RestAccessDeniedHandler accessDeniedHandler; @Autowired private MyFilterSecurityInterceptor myFilterSecurityInterceptor; @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder()); } @Override protected 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" ) .loginProcessingUrl("/api/v1/auth/login" ) .usernameParameter("username" ) .passwordParameter("password" ) .permitAll() .successHandler(successHandler) .failureHandler(failHandler) .and() .logout() .permitAll() .and() .authorizeRequests() .anyRequest() .authenticated() .and() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .exceptionHandling().accessDeniedHandler(accessDeniedHandler) .and() .addFilterBefore(new WebSecurityCorsFilter(), ChannelProcessingFilter.class ) .addFilterBefore (myFilterSecurityInterceptor , FilterSecurityInterceptor .class ) //添加JWT 过滤器 除/login 其它请求都需经过此过滤器 .addFilter (new JWTAuthenticationFilter (authenticationManager ())) ; } }