参考项目:
**主要功能:**用户认证(就是用户名密码验证我们系统有这个用户)、用户授权(就是判断这个用户是不是有这个权限)
需要实现:
1、登录
2、token生成及验证,保存管理等等
3、角色管理体系
4、ip白名单
配置文件:
需要重写实现的地方:
解决的问题:
1、根据deploy.mode配置,决定是省端的青天模式,门户模式,还是地市自己的用户系统模式
配置以下注解
@ConditionalOnProperty(name={"deploy.mode"}, havingValue = "system")
就是除了模式是system的时候,其他时候不去创建这个bean就可以实现
了解:
主体就是这个config文件,通过配置一些过滤器,来实现用户校验、ip限制啥的
1、web拦截
这边配置了html、ico、css等文件的访问
2、配置拦截器
每个filter怎么拦截的,可以看下面附录的代码备注
3、框架的密码加密校验
自定义一个加密方式,继承于PasswordEncoder,重写match方法,再controller方法中指定使用这种判断方式
public class MD5PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return Md5Util.getMd5(charSequence.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (encodedPassword != null && encodedPassword.length() != 0) {
if (!encodedPassword.equalsIgnoreCase(encode(rawPassword))) {
return false;
} else {
return true;
}
} else {
return false;
}
}
}
4、重写UserDetailServiceImpl类的loadUserByUsername
就是根据用户名获取用户信息返回用户
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser dbUsers = getDbUser(username);
if (!username.equals(dbUsers.getUsername())) {
throw new UsernameNotFoundException(String.format("该用户不存在 '%s'.", username));
}
SecurityUser securityUser = null;
try {
securityUser = new SecurityUser(dbUsers);
} catch (InvocationTargetException | IllegalAccessException e) {
logger.error("构建security用户失败", e);
}
if(securityUser == null){
throw new UsernameNotFoundException(String.format("该用户不存在 '%s'.", username));
}
if (BaseConstants.FOUR.equals(securityUser.getStatus())) {
//用户被禁用
securityUser.setAccountNonLocked(Boolean.FALSE);
} else if (BaseConstants.TWO.equals(securityUser.getStatus())) {
//用户被注销
securityUser.setEnabled(Boolean.FALSE);
}
return securityUser;
}
5、一些安全措施
问题:
1、jwt登录头验证,如果地址在白名单url里面,jwt直接跳过
如果不在白名单,且传的token为空也可以通过
如果不在白名单,且传的token错误则不通过
2、
附录:
1、配置文件:securityConfig
package com.meiya.assistance.web.auth.config;
import com.meiya.assistance.web.auth.encoder.MD5PasswordEncoder;
import com.meiya.assistance.web.auth.encoder.SM4PasswordEncoder;
import com.meiya.assistance.web.auth.filter.JwtAuthorizationTokenFilter;
import com.meiya.assistance.web.auth.properties.IgnoredUrlsProperties;
import com.meiya.assistance.web.auth.service.impl.UserDetailsServiceImpl;
import com.meiya.basic.constant.BaseConstant;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
/**
* 系统权限配置
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@ConditionalOnProperty(name={"deploy.mode"}, havingValue = "system")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${account.passwordEncode}")
private String passwordEncodeType;
@Autowired
private IgnoredUrlsProperties ignoredUrlsProperties;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
JwtAuthorizationTokenFilter authenticationTokenFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoderBean());
}
@Bean
public PasswordEncoder passwordEncoderBean() {
// 根据配置判断,是使用哪种加密方式
if(StringUtils.equals(passwordEncodeType, BaseConstant.MD5)){
return new MD5PasswordEncoder();
} else {
return new SM4PasswordEncoder();
}
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
HttpMethod.GET,
"/*.html",
"/favicon.ico",
"/**/*.html",
"/**/*.css",
"/**/*.js"
);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
.authorizeRequests();
// ----------------------------------------1、url白名单,有配置的话这边直接permitAll跳过了-------------------------------------
//除配置文件忽略路径其它所有请求都需经过认证和授权
for (String url : ignoredUrlsProperties.getUrls()) {
registry.antMatchers(url).permitAll();
}
// ------------------------------------------2、用户token校验,这边叫做Authorization里面可以改的---------
registry.and()
.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// ------------------------------------------3、------------------------------------------------
// security中启用跨域访问 !!!因为项目是前后端分离的,所以要解决跨域问题,否则vue访问不了后端,这边就开启了
//需要注意的是,为了防止跨站攻击
registry.and()
.cors()
.and()
.csrf().disable();
// ------------------------------------------4、------------------------------------------------
// 禁用security page cache
registry.and()
.headers()
.frameOptions().sameOrigin()
.cacheControl();
// 通过添加Content-Security-Policy拦截 !!!xss攻击
http.headers()
.xssProtection()
.and()
.contentSecurityPolicy("script-src 'self'");
}
}
2、加密工具
package com.xxxxx.assistance.web.config.security;
import com.xxxxx.basic.constant.BaseConstant;
import com.xxxxx.framework.common.util.Md5Util;
import com.xxxxx.portal.util.sm4.SM4Utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* SM4加密
* @Author
* @Date 2022/10/14 10:53
*/
public class SM4PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return SM4Utils.sm4EncForCBC(BaseConstant.SM4_DES_KEY, charSequence.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if(StringUtils.isBlank(encodedPassword)){
return false;
}
if (!encodedPassword.equalsIgnoreCase(encode(rawPassword))) {
return false;
} else {
return true;
}
}
}
3、jwt过滤器
package com.xxxxx.assistance.web.auth.filter;
import com.xxxxx.assistance.common.cache.LoginCache;
import com.xxxxx.assistance.web.auth.domain.SecurityUser;
import com.xxxxx.assistance.web.auth.properties.IgnoredUrlsProperties;
import com.xxxxx.assistance.web.auth.utils.JwtTokenUtil;
import com.xxxxx.assistance.web.auth.utils.SecurityContextUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
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.List;
import java.util.Objects;
@Component
@Slf4j
@ConditionalOnProperty(name={"deploy.mode"}, havingValue = "system")
public class JwtAuthorizationTokenFilter extends OncePerRequestFilter {
@Autowired
@Qualifier("userDetailService")
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Value("${jwt.header}")
private String tokenHeader;
@Autowired
private LoginCache loginCache;
@Autowired
private IgnoredUrlsProperties ignoredUrlsProperties;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException, ExpiredJwtException {
log.debug("processing authentication for '{}'", request.getRequestURL());
AntPathMatcher matcher = new AntPathMatcher();
String uri = request.getRequestURI();
for (String url : ignoredUrlsProperties.getUrls()) {
if (matcher.match(url, uri)) {
chain.doFilter(request, response);
return;
}
}
final String requestHeader = request.getHeader(this.tokenHeader);
String username = null;
String authToken = null;
if (requestHeader != null && requestHeader.startsWith("Bearer ")) {
authToken = requestHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (ExpiredJwtException e) {
log.info("token expired:{}", e);
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, "token expired");
return;
} catch (Exception e) {
log.info("invalide token:{}", e);
response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "invalide token");
return;
}
} else {
log.warn("couldn't find bearer string, will ignore the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
log.debug("security context was null, so authorizating user");
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
List<GrantedAuthority> authorities = jwtTokenUtil.getAuthorities(authToken);
// 兼容门户对接的角色设置
if (!authorities.isEmpty()) {
((SecurityUser) userDetails).setAuthorities(authorities);
}
//token校验合规则未被挤出
boolean isValidToken = jwtTokenUtil.validateToken(authToken, userDetails);
String tokenInCache = loginCache.get(SecurityContextUtil.getLoginKey(username));
if (StringUtils.isBlank(tokenInCache)) {
// 没有缓存,表示正常退出
response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED, "has been logout");
return;
}
boolean isExistInCache = Objects.nonNull(authToken) && authToken.equals(tokenInCache);
if (isValidToken && isExistInCache) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("authorizated user '{}', setting security context", username);
SecurityContextHolder.getContext().setAuthentication(authentication);
} else if (!isValidToken) {
response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE, "invalide token");
return;
} else if (isValidToken && !isExistInCache) {
response.sendError(419, "has been logout");
return;
}
}
chain.doFilter(request, response);
}
}