还能不动声色饮茶,踏碎这一场,盛世烟花
记录一下,初学者
SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的
应用:身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。
JWT token的格式:header.payload.signature
这个博客对参数和方法描述的比较详细
解析token
token保存在客户端。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
<dependency>
<groupId>com.github.axetgroupId>
<artifactId>kaptchaartifactId>
<version>0.0.9version>
dependency>
# JWT储存的请求头
jwt.tokenHeader=Authorization
# JWT加解密使用的密钥
jwt.secret=my-secret
# JWT 的超期限时间(60*60*24)
jwt.expiration=604800
# JWT 荷载中拿到的开头
jwt.tokenHead=Bearer
主要用于一些对token的操作:
JwtTokenUtil.java
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtTokenUtil {
//定义用户名和过期时间
private static final String CLAIM_KEY_USERNAME="sub";
private static final String CLAIM_KEY_CREATED="created";
@Value("${jwt.secret}")
private String secret; // jwt 加解密使用的密钥
@Value("${jwt.expiration}")
private Long expiration; //jwt 的过期时间 (60*60*24)24小时失效
/**
*1.根据用户信息生成token
* 用户信息可以在security框架的UserDetails里面获取
* @param userDetails
* @return
*/
public String generateToken(UserDetails userDetails){
Map<String,Object> claims = new HashMap<>(); //准备存放token的容器(荷载)
claims.put(CLAIM_KEY_USERNAME,userDetails.getUsername()); //从security框架的UserDetails 中获取用户名
claims.put(CLAIM_KEY_CREATED,new Date()); //创建时间为当前时间
return generateToken(claims); //增加其他信息(本类内新建方法)
}
/**
* 4. 从token 中获取登录用户名
* @param token
* @return
*/
public String getUserNameFromToken(String token){
String username;
try {
Claims claims = getClaimsFromToken(token); //根据token 获取荷载(本类内新建方法)
username = claims.getSubject(); //通过荷载调用 getSubject方法,获取用户名
} catch (Exception e) {
username = null; //有异常username为空
}
return username;
}
/**
* 6. 验证token是否有效
* a。判断token是否过期
* b。token荷载中的用户名和userDetails中的用户名是否一致
* @param token
* @param userDetails
* @return
*/
public boolean validateToken(String token,UserDetails userDetails){
String username = getUserNameFromToken(token); //从token中获取用户名
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);//从token中拿到的用户名和用户信息对比,并判断token没有过期
}
/**
*9.判断token是否可以被刷新
* 过期了,可以刷新。获取有效时间方法取反为过期
* @param token
* @return
*/
public boolean canRefresh(String token){
return !isTokenExpired(token);
}
/**
* 10.刷新token过期时间
* @param token
* @return
*/
public String refreshToken(String token){
Claims claims = getClaimsFromToken(token);
claims.put(CLAIM_KEY_CREATED,new Date()); //通过荷载设置创建时间改为当前时间 = 刷新token过期时间
return generateToken(claims); //生成token
}
/**
* 7.判断token 是否失效
* @param token
* @return
*/
private boolean isTokenExpired(String token) {
Date expireDate = getExpirationDateFromToken(token);//获取token的失效时间
return expireDate.before(new Date()); //如果token 过期时间在当前时间前面,有效。
}
/**
* 8.从token中拿到荷载,在获得过期时间
* @param token
* @return
*/
private Date getExpirationDateFromToken(String token) {
Claims claims = getClaimsFromToken(token);
return claims.getExpiration();
}
/**
* 5。通过解析token得到荷载
* @param token
* @return
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
claims = Jwts.parser()
.setSigningKey(secret) //与builder中签名方法signWith()对应,设置签名(密钥)的key
.parseClaimsJws(token) 解析令牌,
.getBody(); //得到解析好的荷载
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* 2.根据荷载生成 JWT token
* @param claims
* @return 私有,供本类使用
*/
private String generateToken(Map<String, Object> claims) {
return Jwts.builder()
.setClaims(claims) //设置荷载
.setExpiration(generateExpirationDate()) //设置过期时间 ,需要类型转换(本类内新建方法)
.signWith(SignatureAlgorithm.ES512,secret) //设置签名方法
.compact(); //生成jwt
}
/**
* 3.生成token 失效时间
* 类型转换
* @return
*/
private Date generateExpirationDate() {
//失效时间为:当前时间+配置的过期时间
return new Date(System.currentTimeMillis() + expiration*1000);
}
}
这个也是最核心的一个配置了。这里简单说明一下重写的3个config。
1.protected void configure(AuthenticationManagerBuilder auth)
AuthenticationManagerBuilder(身份验证管理生成器)
用来配置认证管理器AuthenticationManager。说白了就是用于配置UserDetailsService及PasswordEncoder;
2.public void configure(WebSecurity web)
WebSecurity(WEB安全)
用来配置 WebSecurity 。而 WebSecurity 是基于 Servlet Filter 用来配置 springSecurityFilterChain 。而 springSecurityFilterChain 又被委托给了 Spring Security 核心过滤BeanDelegatingFilterProxy
我们一般不会过多来自定义 WebSecurity , 使用较多的使其ignoring()
方法用来忽略 Spring Security 对静态资源的控制。
3.protected void configure(HttpSecurity http)
HttpSecurity(HTTP请求安全处理)
HttpSecurity 用于构建一个安全过滤器链 SecurityFilterChain 。SecurityFilterChain 最终被注入核心过滤器 。 HttpSecurity 有许多我们需要的配置。我们用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器
常用方法
方法 | 说明 |
---|---|
openidLogin() | 用于基于 OpenId 的验证 |
headers() | 将安全标头添加到响应,比如说简单的 XSS 保护 |
cors() | 配置跨域资源共享( CORS ) |
sessionManagement() | 允许配置会话管理 |
portMapper() | 允许配置一个PortMapper(HttpSecurity#(getSharedObject(class))),其他提供SecurityConfigurer的对象使用 PortMapper 从 HTTP 重定向到 HTTPS 或者从 HTTPS 重定向到 HTTP。默认情况下,Spring Security使用一个PortMapperImpl映射 HTTP 端口8080到 HTTPS 端口8443,HTTP 端口80到 HTTPS 端口443 |
jee() | 配置基于容器的预认证。 在这种情况下,认证由Servlet容器管理 |
x509() | 配置基于x509的认证 |
rememberMe() | 允许配置“记住我”的验证 |
authorizeRequests() | 允许基于使用HttpServletRequest限制访问 |
requestCache() | 允许配置请求缓存 |
exceptionHandling() | 允许配置错误处理 |
securityContext() | 在HttpServletRequests之间的SecurityContextHolder上设置SecurityContext的管理。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
servletApi() | 将HttpServletRequest方法与在其上找到的值集成到SecurityContext中。 当使用WebSecurityConfigurerAdapter时,这将自动应用 |
csrf() | 添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用 |
logout() | 添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success” |
anonymous() | 允许配置匿名用户的表示方法。 当与WebSecurityConfigurerAdapter结合使用时,这将自动应用。 默认情况下,匿名用户将使用org.springframework.security.authentication.AnonymousAuthenticationToken表示,并包含角色 “ROLE_ANONYMOUS” |
formLogin() | 指定支持基于表单的身份验证。如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面 |
oauth2Login() | 根据外部OAuth 2.0或OpenID Connect 1.0提供程序配置身份验证 |
requiresChannel() | 配置通道安全。为了使该配置有用,必须提供至少一个到所需信道的映射 |
httpBasic() | 配置 Http Basic 验证 |
addFilterBefore() | 在指定的Filter类之前添加过滤器 |
addFilterAt() | 在指定的Filter类的位置添加过滤器 |
addFilterAfter() | 在指定的Filter类的之后添加过滤器 |
and() | 连接以上策略的连接器,用来组合安全策略。实际上就是"而且"的意思 |
import com.xxx.server.config.security.component.*;
import com.xxx.server.pojo.Admin;
import com.xxx.server.service.AdminService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AdminService adminService;
@Autowired
private RestAuthorizationEntryPoint restAuthorizationEntryPoint; // 未登录 token 失效时自定义处理结果
@Autowired
private RestfulAccessDeniedHandler restfulAccessDeniedHandler; // 无权访问时自定义处理结果
@Autowired
private CustomFilter customFilter;
@Autowired
private CustomUrlDecisionManager customUrlDecisionManager;
// 1、重写 UserDetailsService,用我们自己写的业务逻辑
@Override
@Bean
public UserDetailsService userDetailsService() {
return username -> {
Admin admin = adminService.getAdminByUserName(username);
if (null != admin) {
admin.setRoles(adminService.getRoles(admin.getId()));
return admin;
}
throw new UsernameNotFoundException("用户名或密码不正确");
};
}
// 2、让 Security 走我们重写的 UserDetailsService ,通过 getAdminByUserName 获取用户名
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
}
// 3、密码加解密对象
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 6、放行路径(不走拦截链)
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/**.html",
"/login/**",
"/hello/**",
"/logout/**",
"/css/**",
"/js/**",
"/img/**",
"/captcha", // 验证码接口
);
}
/**
* 4、SpringSecurity 配置
*
* @param http
* @throws Exception
*/
/**
* anyRequest | 匹配所有请求路径
* access | SpringEl表达式结果为true时可以访问
* anonymous | 匿名可以访问
* denyAll | 用户不能访问
* fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority | 如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole | 如果有参数,参数表示角色,则其角色可以访问
* permitAll | 用户可以任意访问
* rememberMe | 允许通过remember-me登录的用户访问
* authenticated | 用户登录后可访问
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 使用 JWT , 不需要 csrf
http.csrf()
.disable()
// 基于 token,不需要 session
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 所有请求都要求认证
.anyRequest()
.authenticated()
// 动态权限配置
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);//权限决策处理类
object.setSecurityMetadataSource(customFilter);//路径(资源)拦截处理
return object;
}
})
.and()
// 禁用缓存
.headers()
.cacheControl();
// 添加 jwt 登录授权过滤器
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
// 添加自定义未授权和未登录结果返回
http.exceptionHandling()
.accessDeniedHandler(restfulAccessDeniedHandler)
.authenticationEntryPoint(restAuthorizationEntryPoint);
}
// 5、JWT 登录授权过滤器
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
这里面的动态权限配置,jwt登录授权过滤器,未授权未登录情况的处理都需要我们自己配置
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* 权限控制
* 判断用户角色
*
* @author
*/
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
//当前url所需要的角色
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)){
if (authentication instanceof AnonymousAuthenticationToken){
throw new AccessDeniedException("尚未登录,请登录");
}else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)){
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return false;
}
@Override
public boolean supports(Class<?> clazz) {
return false;
}
}
import com.xxx.server.pojo.Menu;
import com.xxx.server.pojo.Role;
import com.xxx.server.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import java.util.Collection;
import java.util.List;
/** 权限控制
* 根据请求 url 分析请求所需的角色
* @author
*/
@Component
public class CustomFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
private MenuService menuService;
AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
// 获取请求的 url
String requestUrl = ((FilterInvocation) object).getRequestUrl();
// 权限角色查询所有菜单
List<Menu> menus = menuService.getMenusWithRole();
// 判断请求 url 与菜单角色是否匹配
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(),requestUrl)) {
String[] str = menu.getRoles().stream().map(Role::getName).toArray(String[]::new);
return SecurityConfig.createList(str);
}
}
// 没匹配的 url 默认登录即可访问 : ROLE_LOGIN 登录即可拥有的角色
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
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.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;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.tokenHeader}")
private String tokenHeader; // JWT 存储的请求头( key )
@Value("${jwt.tokenHead}")
private String tokenHead; // JWT 负载中拿到开头( value )
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
// 前置拦截器
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader(tokenHeader); // 根据 key 获取 value(要验证的头)
// 存在 token
// 如果拿到 value,并且是根据 Bearer 开头的
if (null != authHeader && authHeader.startsWith(tokenHead)) {
// 截取字符串
String authToken = authHeader.substring(tokenHead.length());
String username = jwtTokenUtil.getUserNameFromToken(authToken);
// token 存在用户名,但未登录( 在 springSecurity 上下文拿不到 用户对象 )
if (null != username && null == SecurityContextHolder.getContext().getAuthentication()) {
// 登录(通过 username 拿到 UserDetails )
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 判断 token 是否有效,重新设置用户对象
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
// 参数:用户对象 密码 角色
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
// 重新设置用户对象到 springSecurity上下文中去
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
// 放行
filterChain.doFilter(request, response);
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.server.utils.RespBean;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
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.io.PrintWriter;
@Component
public class RestfulAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("权限不足,请联系管理员!");
bean.setCode(403);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xxx.server.utils.RespBean;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
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.io.PrintWriter;
@Component
public class RestAuthorizationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
PrintWriter out = response.getWriter();
RespBean bean = RespBean.error("您尚未登录,请登录!");
bean.setCode(401);
out.write(new ObjectMapper().writeValueAsString(bean));
out.flush();
out.close();
}
}