spring security
Spring Security的Servlet支持基于Servlet过滤器,因此首先了解过滤器的作用是很有帮助的。下图显示了针对单个HTTP请求的典型处理程序的分层。
Spring provides a Filter implementation named DelegatingFilterProxy that allows bridging between the Servlet container’s lifecycle and Spring’s ApplicationContext. The Servlet container allows registering Filters using its own standards, but it is not aware of Spring defined Beans. DelegatingFilterProxy can be registered via standard Servlet container mechanisms, but delegate all the work to a Spring Bean that implements Filter.
Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext之间建立桥接。Servlet容器允许使用它自己的标准注册过滤器,但是它不知道Spring定义的bean。DelegatingFilterProxy可以通过标准的Servlet容器机制注册,但将所有工作委托给实现过滤器的Spring Bean
伪代码
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// Lazily get Filter that was registered as a Spring Bean
// For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
Filter delegate = getFilterBean(someBeanName);
// delegate work to the Spring Bean
delegate.doFilter(request, response);
}
FilterChainProxy使用SecurityFilterChain来确定应该为该请求调用哪个Spring安全过滤器。
FilterChainProxy中包含多个filterChain
一般servlet通常根据url来匹配filter
FilterChainProxy可以通过利用RequestMatcher接口来基于HttpServletRequest中的任何内容确定调用。
在多个SecurityFilterChain图中,FilterChainProxy决定应该使用哪个SecurityFilterChain。只有第一个匹配的SecurityFilterChain将被调用。如果一个URL /api/messages/被请求,它将首先匹配SecurityFilterChain0的模式/api/**,所以只有SecurityFilterChain0将被调用,即使它也匹配SecurityFilterChainN。如果URL /messages/被请求,它将与SecurityFilterChain0的模式/api/**不匹配,因此FilterChainProxy将继续尝试每个SecurityFilterChain。
下面是Spring Security Filter排序的综合列表:
ChannelProcessingFilter
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
CorsFilter
CsrfFilter
LogoutFilter
OAuth2AuthorizationRequestRedirectFilter
Saml2WebSsoAuthenticationRequestFilter
X509AuthenticationFilter
AbstractPreAuthenticatedProcessingFilter
CasAuthenticationFilter
OAuth2LoginAuthenticationFilter
Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
OpenIDAuthenticationFilter
DefaultLoginPageGeneratingFilter
DefaultLogoutPageGeneratingFilter
ConcurrentSessionFilter
DigestAuthenticationFilter
BearerTokenAuthenticationFilter
BasicAuthenticationFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
JaasApiIntegrationFilter
RememberMeAuthenticationFilter
AnonymousAuthenticationFilter
OAuth2AuthorizationCodeGrantFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
SwitchUserFilter
身份认证
组件介绍
SecurityContextHolder - The SecurityContextHolder is where Spring Security stores the details of who is authenticated.
SecurityContextHolder是Spring Security存储身份验证人员详细信息的地方。
SecurityContext - is obtained from the SecurityContextHolder and contains the Authentication of the currently authenticated user.
从SecurityContextHolder获得,并包含当前已验证用户的身份验证
Authentication - Can be the input to AuthenticationManager to provide the credentials a user has provided to authenticate or the current user from the SecurityContext.
可以是AuthenticationManager的输入,以提供用户为进行身份验证而提供的凭证,或来自SecurityContext的当前用户
GrantedAuthority - An authority that is granted to the principal on the Authentication (i.e. roles, scopes, etc.)
在身份验证中授予主体的权限(即角色、范围等)。
AuthenticationManager - the API that defines how Spring Security’s Filters perform authentication.
定义Spring Security过滤器如何执行身份验证的API。
ProviderManager - the most common implementation of AuthenticationManager.
AuthenticationManager的最常见实现
AuthenticationProvider - used by ProviderManager to perform a specific type of authentication.
由ProviderManager用于执行特定类型的身份验证。
Request Credentials with AuthenticationEntryPoint - used for requesting credentials from a client (i.e. redirecting to a log in page, sending a WWW-Authenticate response, etc.)
用于从客户端请求凭据(例如,重定向到登录页面,发送WWW-Authenticate响应,等等)
AbstractAuthenticationProcessingFilter - a base Filter used for authentication. This also gives a good idea of the high level flow of authentication and how pieces work together.
用于身份验证的基本过滤器。这也让我们很好地了解了高层次的身份验证流以及各个部分如何协同工作。
实现
本文直接新建一个过滤器,而不用默认的UsernamePasswordAuthenticationFilter 从而绕过复杂的实现(太菜了,看不懂)
基本流程
发起请求===>到达所建过滤器===>根据url判断是否执行流程(登录,登出url不执行流程)=>更新token有效时间=>继续其他默认过滤器
验证token逻辑
新建一个类SimpleUserPasswordFilter 继承OncePerRequestFilter 并交给spring ioc容器(token验证过滤器)
首先检查请求的url地址:登录,登出url不执行流程;其他地址,校验携带token是否有效
如果无效(包括超时,非法等等)直接中断当前过滤器链,返回响应码401,前端对应处理逻辑:收到401码跳转至登录界面
如果有效则进入下一个过滤器
import xx.xx.xx.core.authentication.springSecurity.component.JwtAuthentication;
import xx.xx.xx.core.jwt.JwtService;
import xx.xx.xx.core.jwt.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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;
@Component
public class SimpleUserPasswordFilter extends OncePerRequestFilter {
@Value("${tokenName}")
private String tokenName;
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getRequestURI().contains("/api/v2/login")||request.getRequestURI().contains("/api/v2/logout")) {
filterChain.doFilter(request, response);
} else {
try {
String token = request.getHeader(tokenName);
if (token!=null&&jwtService.verifyToken(token)) {
String username = jwtService.getUsername(token);
if (username != null) {
UserEntity userEntity = jwtService.queryUserByUserName(username);
String roles = userEntity.getRole();
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
if (!roles.isEmpty()) {
String[] r = roles.split(",");
for (int i = 0; i < r.length; i++) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(r[i]));
}
}
JwtAuthentication jwtAuthentication = new JwtAuthentication(username, null, simpleGrantedAuthorities, true);
SecurityContextHolder.getContext().setAuthentication(jwtAuthentication);
filterChain.doFilter(request, response);
} else {
response.setStatus(401);//token 无效返回401前端重新登录
}
} else {
response.setStatus(401);//token 无效 登出重新登录
}
}catch (Exception e){
e.printStackTrace();
response.setStatus(401);
}
}
}
}
新建一个类UpdateTokenFilter 继承OncePerRequestFilter 并交给spring ioc容器(token有效期更新过滤器)
更新token有效时间
import xx.xx.xx.core.jwt.JwtService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class UpdateTokenFilter extends OncePerRequestFilter {
@Value("${tokenName}")
private String tokenName;
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication!=null&&authentication.isAuthenticated()) {
String token = request.getHeader(tokenName);
if (token != null) {
jwtService.updateToken(token);
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
filterChain.doFilter(request, response);
}
}
}
配置至springsecurity
新建一个类SpringSecurityConfig 继承WebSecurityConfigurerAdapter
添加注解@EnableWebSecurity
其中有3个configure方法 提供不同层次的自定义配置
1.configure(HttpSecurity http):基本配置
2.configure(AuthenticationManagerBuilder auth):如果使用某些默认过滤器,并且配置自己的实现则要重写此方法
3.configure(WebSecurity web):主要用来配置放行一些静态资源,这边不管理静态资源,由nginx管理所有这里不配置
import xx.xx.xx.core.authentication.springSecurity.filter.SimpleUserPasswordFilter;
import xx.xx.xx.core.authentication.springSecurity.filter.UpdateTokenFilter;
import org.springframework.context.ApplicationContext;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
ApplicationContext applicationContext = super.getApplicationContext();
SimpleUserPasswordFilter simpleUserPasswordFilter = applicationContext.getBean(SimpleUserPasswordFilter.class);
UpdateTokenFilter updateTokenFilter = applicationContext.getBean(UpdateTokenFilter.class);
http.authorizeRequests()
.antMatchers("/api/v2/login").permitAll() // 放行登录路径
.antMatchers("/api/v2/logout").permitAll() // 放行登出路径
.antMatchers(HttpMethod.OPTIONS, "/**").anonymous() // 前后端分离项目跨域请求时会分2次发送,这里是第一次发送的options请求 注意所有请求请指定请求类型,如:post/get 不要使用@requestMapping否则有安全隐患
.anyRequest().authenticated() // 其他所有请求需要验证token
.and()
.logout().disable().formLogin().disable() // 关闭默认登录 登出过滤器
.requestCache().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //关闭默认session 这里通过jwt token管理用户状态
.and()
.csrf().disable() // 关闭跨站攻击过滤器 可以开启
.cors()// 启用跨域配置
.and()
.addFilterBefore(simpleUserPasswordFilter, UsernamePasswordAuthenticationFilter.class)// 添加token验证过滤器
.addFilterAfter(updateTokenFilter,SimpleUserPasswordFilter.class);// 添加token更新过滤器
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// ApplicationContext applicationContext = super.getApplicationContext();
// JwtUserDetailsService JwtUserDetailsService = applicationContext.getBean(JwtUserDetailsService.class);
// AuthenticateProvider authenticateProvider = applicationContext.getBean(AuthenticateProvider.class);
// auth.authenticationProvider(authenticateProvider).userDetailsService(JwtUserDetailsService);
}
/**
* 主要用来配置放行一些静态资源
* 这边不管理静态资源,由nginx管理 这里不配置
*/
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
}
登录 登出url
@RestController
@RequestMapping("/api/v2")
public class LoginController {
@Value("${tokenName}")
private String tokenName;
@Autowired
private JwtService jwtService;
@PostMapping("login")
public HashMap login(HttpServletRequest request,HttpServletResponse response){
String username = request.getParameter("username");
String password = request.getParameter("password");
HashMap<String, Object> map = new HashMap<>();
if(jwtService.verifyUsernamePassword(username,password)) {
String token = jwtService.createToken(username);
map.put("data", "login-success");
map.put("name", username);
map.put("Access-Token", token);
}else {
response.setStatus(400);
map.put("message", "用户名/密码不匹配");
}
return map;
}
@RequestMapping("logout")
public Object logout(HttpServletRequest request,HttpServletResponse response){
String token = request.getHeader(tokenName);
if(token!=null) {
jwtService.invalidToken(token);
}
return "success";
}
jwt token
JwtService
提供创建,验证,更新,失效token功能
创建token时有2个超时时间:第一个是token最大保存时间,可以理解为发纸质一张门票,30天后就烂了
另一个是token有效时间(30分钟) 如果用户登录后30分钟无操作(即没有发起过请求,没有经过token更新过滤器)则token无效.相当于你买了张明天的电影票,明天不看后天就不能用了,但是门票一个月后才烂.
其中uuid用来保证,一个账号同一时间只能供一个人用
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import xx.xx.xx.common.exception.IllegalUserException;
import xx.xx.xx.core.authentication.UserInfoManager;
import xx.xx.xx.core.authentication.entity.UserInfo;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.UUID;
@Service
public class JwtService {
@Value("${secret}")
private String secret;
@Value("${expire}")
private String expire;
private final String user ="user";
private final String uuid ="uuid";
@Autowired
private UserInfoManager userInfoInMemoryManager;
@Resource
private UserManageDao userManageDao;
/**
* 生成jwt token
* 在内存中保存用户登录信息,包括:
* token生成时间,过期时间
* token默认保存30天,30分钟不操作强制注销token
*/
public String createToken (String userName){
Date now = new Date();
Date expireDate = new Date(now.getTime() + Long.parseLong(expire)*60*1000);//过期时间 分钟
Date storeDate = new Date(now.getTime() + 1000L*60*60*24*30);//最大保存时间 30天
UUID randomUuid = UUID.randomUUID();
JWTCreator.Builder builder = JWT.create();
String token = builder.withExpiresAt(storeDate).withIssuedAt(now)
.withClaim(user, userName)
.withClaim(uuid,randomUuid.toString())
.sign(Algorithm.HMAC512(secret));
userInfoInMemoryManager.create(new UserInfo().setUserName(userName).setUuid(randomUuid.toString()).setExpireTime(expireDate.getTime()));
return token;
}
/**
* 验证uuid是否一致 每个uuid代表该用户一次登录操作,同时只能由一人使用
* 验证是否过期
*/
public boolean verifyToken (String token){
boolean flag =false;
String userName = JWT.decode(token).getClaim(user).asString();
JWTVerifier verifier = JWT.require(Algorithm.HMAC512(secret)).build();
try {
DecodedJWT d = verifier.verify(token);
flag = userInfoInMemoryManager.validateIsExpired(new UserInfo().setUserName(userName).setUuid(d.getClaim(uuid).asString()));
}catch (JWTVerificationException e){
e.printStackTrace();
} catch (IllegalUserException e) {
e.printStackTrace();
}
return flag;
}
/**
* 验证用户名密码
* 密码传输时暂时为明文
* 通过hmacMD5算法 加盐加密后存储到数据库
*/
public boolean verifyUsernamePassword (String username,String password){
boolean flag =false;
try {
UserEntity user = userManageDao.getUserInfoByUsername(new UserEntity().setUsername(username));
String salt = user.getSalt();
String psdInDb = user.getPassword();
byte[] secret = Hex.decodeHex(salt);
SecretKey restoreSecretKey = new SecretKeySpec(secret, "hmacMD5");
Mac mac = Mac.getInstance(restoreSecretKey.getAlgorithm());//实例化mac
mac.init(restoreSecretKey);//初始化mac
byte[] result = mac.doFinal(password.getBytes());//执行摘要
String s = Hex.encodeHexString(result);
if(s.equals(psdInDb)){
flag=true;
}
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
public UserEntity queryUserByUserName(String username){
return userManageDao.getUserInfoByUsername(new UserEntity().setUsername(username));
}
/**
* 更新当前用户token持续时间,只有有效的token 才会走到这步
*/
public void updateToken (String token){
String userName = JWT.decode(token).getClaim(user).asString();
String uuidStr = JWT.decode(token).getClaim(uuid).asString();
userInfoInMemoryManager.update(new UserInfo().setUserName(userName).setUuid(uuidStr));
}
public String getUsername (String token){
return JWT.decode(token).getClaim(user).asString();
}
public void invalidToken (String token){
String userName = JWT.decode(token).getClaim(user).asString();
userInfoInMemoryManager.expireUser(new UserInfo().setUserName(userName));
}
userInfoInMemoryManager.create(new UserInfo().setUserName(userName).setUuid(randomUuid.toString()).setExpireTime(expireDate.getTime()));
这里是将用户登录信息存入本地内存,如果有其他服务存到redis以达到共享内存的效果
@Service
public class UserInfoInMemoryManager implements UserInfoManager {
private final Map<String,UserInfo> userInfoMap = new ConcurrentHashMap<>(8);
@Value("${expire}")
private String expire;
@Override
public void create(UserInfo info) {
if(null == userInfoMap.get(info.getUserName())){
userInfoMap.put(info.getUserName(), info);
}else {
update(info);
}
}
@Override
public void update(UserInfo info) {
info.setExpireTime(System.currentTimeMillis()+Long.parseLong(expire)*60*1000);
userInfoMap.put(info.getUserName(), info);
}
@Override
public boolean validateIsExpired(UserInfo info) throws IllegalUserException {
boolean flag =false;
if(userInfoMap.get(info.getUserName())!=null){
if(!userInfoMap.get(info.getUserName()).getUuid().equals(info.getUuid())){
throw new IllegalUserException("非法请求");
}
flag = System.currentTimeMillis() <= userInfoMap.get(info.getUserName()).getExpireTime();
}
return flag;
}
@Override
public void expireUser(UserInfo info) {
userInfoMap.remove(info.getUserName());
}
}
验证权限
本文并没有使用springsecurity提供的注解而是自定义
校验工具类
public class AuthorizationUtil {
public static boolean hasRole(FunctionModule... roles){
boolean flag =false;
try {
Collection<? extends GrantedAuthority> authorities = SecurityContextHolder.getContext().getAuthentication().getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
while (iterator.hasNext()){
GrantedAuthority next = iterator.next();
for(int i=0;i<roles.length;i++) {
if (next.getAuthority().equals(roles[i].name())) {
flag = true;
break;
}
}
if(flag){
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
return flag;
}
}
权限功能的使用
例如创建一个新用户,该接口只能由有系统配置权限的用户使用
@PostMapping("createUser")
public Object createUser(HttpServletRequest request, HttpServletResponse response){
HashMap<String, String> map = new HashMap<>();
if(AuthorizationUtil.hasRole(FunctionModule.All,FunctionModule.SystemConfiguration)){
String username = request.getParameter("username");
String password = request.getParameter("password");
String role = request.getParameter("role");
if(userManagerService.createUser(username,password,role)){
map.put("message","创建成功");
map.put("result","success");
}else {
map.put("result","error");
map.put("message", "创建失败");
}
}else {
response.setStatus(403);
}
return map;
}
FunctionModule 是个枚举类 记录了所有权限类型
public enum FunctionModule {
NeConfiguration,Console,SystemConfiguration,All
}
登录时根据用户权限 生成vue-router 的动态路由
隐去了 具体业务逻辑
@Service
public class GenerateMenuServiceImpl {
@Autowired
private NeAccessServiceImpl neAccessServiceImpl;
@Autowired
private NeConfgServiceImpl neConfgServiceImpl;
@Autowired
private DataTemplateServiceImpl dataTemplateServiceImpl;
public ArrayList<Menu> GenerateMenu() {
ArrayList<Menu> menuArrayList = new ArrayList<>();
GenerateIndex(menuArrayList);
if(AuthorizationUtil.hasRole(FunctionModule.NeConfiguration,FunctionModule.All)){
GenerateXX1(menuArrayList);
GenerateXX2(menuArrayList);
}
if(AuthorizationUtil.hasRole(FunctionModule.Console,FunctionModule.All)){
GenerateXX3(menuArrayList);
}
if(AuthorizationUtil.hasRole(FunctionModule.SystemConfiguration,FunctionModule.All)){
GenerateSystemConfiguration(menuArrayList);
}
return menuArrayList;
}
其他
springsecurity 提供了一个SecurityContextHolder 基于threadlocl的上下文
里面存储了当前请求对应的用户信息
该上下文每个请求完成后都会被删除,所以每次请求都要去生成
本文是SimpleUserPasswordFilter 实现的
验证通过后存储到SecurityContextHolder 供后面使用
判断权限时从SecurityContextHolder 获取用户信息
部分具体实现类没有贴出来,需要根据具体数据库结构,业务逻辑自己补充
或者人多了再贴