SpringSecurity
SpringSecurity是一个强大的可高度定制的认证和授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。
JWT
JWT是JSON WEB TOKEN的缩写,它是基于 RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT的组成
- JWT token的格式:header.payload.signature
- header中用于存放签名的生成算法
{"alg": "HS512"}
- payload中用于存放用户名、token的生成时间和过期时间
{"sub":"admin","created":1489079981393,"exp":1489684781}
- signature为以header和payload生成的签名,一旦header和payload被篡改,验证将失败
//secret为加密算法的密钥
String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
JWT实例
这是一个JWT的字符串
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw
可以在该网站上获得解析结果:https://jwt.io/
1.Web安全配置:
package com.auth.authserver.config;
import com.auth.authserver.filter.JwtLoginFilter;
import com.auth.authserver.filter.JwtVerifyFilter;
import com.auth.authserver.handle.MyAuthenticationEntryPoint;
import com.auth.authserver.handle.MyAccessDeniedHandle;
import com.auth.authserver.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author Json
* @date 2021/10/29 15:08
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private PasswordEncoder passwordEncoder;
private UserService userService;
private MyAccessDeniedHandle myAccessDeniedHandle;
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@Autowired
public void setRestfulAccessDeniedHandle(MyAccessDeniedHandle myAccessDeniedHandle) {
this.myAccessDeniedHandle = myAccessDeniedHandle;
}
@Autowired
public void setRestAuthenticationEntryPoint(MyAuthenticationEntryPoint myAuthenticationEntryPoint) {
this.myAuthenticationEntryPoint = myAuthenticationEntryPoint;
}
@Bean
public PasswordEncoder myPasswordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* Remove the ROLE_ prefix
*/
@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 允许访问
.antMatchers("/login").permitAll()
.anyRequest().authenticated() // 其他请求拦截
.and()
.csrf().disable() //关闭csrf
.addFilter(new JwtLoginFilter(super.authenticationManager()))
.addFilter(new JwtVerifyFilter(super.authenticationManager()))
.exceptionHandling()
.accessDeniedHandler(myAccessDeniedHandle) // 自定义无权限访问
.authenticationEntryPoint(myAuthenticationEntryPoint) // 自定义未登录返回
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); //禁用session
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// UserDetailsService类
auth.userDetailsService(userService)
// 加密策略
.passwordEncoder(passwordEncoder);
}
/**
* 解决 AuthenticationManager 无法注入的问题
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2.定义接口UserService去继承UserDetailsService
/**
* @author Json
* @date 2021/10/29 15:11
*/
public interface UserService extends UserDetailsService {
}
3.定义实现类UserServiceImpl 实现UserService
package com.auth.authserver.service.impl;
import com.auth.authserver.service.UserService;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* @author Json
* @date 2021/10/29 15:11
*/
@Service
public class UserServiceImpl implements UserService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List authorities = new ArrayList<>();
// 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
authorities.add(new SimpleGrantedAuthority("ADMIN"));
// 临时写死 这里是数据库查询出来的
return User.builder().username("admin")
.password(passwordEncoder.encode("123456"))
.authorities(authorities).build();
}
}
4.自定义用户名密码登录,也就是UsernamePasswordAuthenticationFilter,重写认证逻辑,其实也就是登录接口,默认地址为"/login" 请求为POST,这里面包含了认证,登录成功该怎么办,登录失败该怎么办,当然也可以自己去定义登录接口,其实到道理是一样的
package com.auth.authserver.filter;
import cn.hutool.json.JSONUtil;
import com.auth.authserver.utils.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* 登录校验
*
* 第一种方式 我们这里用框架自带的 过滤器实现
* 第二种方式 可以自己实现登录接口 去认证 其实也是 AuthenticationManager。authenticate 去认证
*
* @author Json
* @date 2021/11/1 13:47
*/
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public JwtLoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 相当于登录 认证
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authenticationToken);
return authenticationManager.authenticate(authenticationToken);
}
/**
* 一旦调用 springSecurity认证登录成功,立即执行该方法
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
//登录成功时,返回json格式进行提示
String username = obtainUsername(request);
String jwt = JwtUtils.createJwt(username);
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>(4);
// 这里写死只做测试 请以实际为主
map.put("code", "200");
map.put("message", "登陆成功!");
map.put("token",jwt);
response.addHeader("Authorization", jwt);
response.getWriter().println(JSONUtil.parse(map));
response.getWriter().flush();
response.getWriter().close();
}
/**
* 一旦调用 springSecurity认证失败 ,立即执行该方法
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) throws IOException {
//登录失败时,返回json格式进行提示
Map map = new HashMap(4);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_BAD_GATEWAY);
PrintWriter out = response.getWriter();
if (ex instanceof BadCredentialsException) {
map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
map.put("message", "账号或密码错误!");
}else {
// 这里还有其他的 异常 。。 比如账号锁定 过期 等等。。。
map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
map.put("message", "登陆失败!");
}
out.write(new ObjectMapper().writeValueAsString(map));
response.getWriter().println(JSONUtil.parse(map));
response.getWriter().flush();
response.getWriter().close();
}
}
5.自定义请求拦截器,为什么呢,难道你可以随意请求吗?配置文件中写好了除了登录接口可以通过,其他请求全部拦截下来。这里是用jwt做的。
package com.auth.authserver.filter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
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;
/**
* 请求校验
*
* @author Json
* @date 2021/11/6 14:34
*/
public class JwtVerifyFilter extends BasicAuthenticationFilter {
public JwtVerifyFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String header = request.getHeader("Authorization");
if (header != null) {
List authorities = new ArrayList<>();
// 用户可以访问的资源名称(或者说用户所拥有的权限) 注意:必须"ROLE_"开头
authorities.add(new SimpleGrantedAuthority("user:resource"));
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken
("admin",null, authorities);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
chain.doFilter(request, response);
}
}
6.增加当访问接口没有权限时的处理
package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
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.util.HashMap;
import java.util.Map;
/**
* 当访问接口没有权限时主要实现springsecurity给我们提供的 AccessDeniedHandler接口,自定义的返回结果
*
* @author Json
* @date 2021/11/10 11:04
*/
@Component
public class MyAccessDeniedHandle implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
// 这里写死只做测试 请以实际为主
Map map = new HashMap<>();
map.put("code", 501);
map.put("msg", "您没有权限");
response.getWriter().println(JSONUtil.parse(map));
response.getWriter().flush();
}
}
7.当未登录或者token失效访问接口时自定义处理
package com.auth.authserver.handle;
import cn.hutool.json.JSONUtil;
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.util.HashMap;
import java.util.Map;
/**
* 当未登录或者token失效访问接口时,自定义的返回结果
*
*
* @author Json
* @date 2021/11/10 11:20
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>(4);
// 这里写死只做测试 请以实际为主
map.put("code", HttpServletResponse.SC_BAD_GATEWAY);
map.put("message", "请登录!");
response.getWriter().println(JSONUtil.parse(map));
response.getWriter().flush();
}
}
附上jwt工具类,当然以实际为主,这里主要做测试,封装的很简单
package com.crm.common.utils;
import com.crm.common.config.JwtConfig;
import com.crm.common.enums.ResultCodeEnum;
import com.crm.common.exception.JwtApiException;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* jwt 工具类
*
* @author Json
* @date 2021/11/15 16:38
*/
@Slf4j
public class JwtUtils {
/**
* jwt 载荷信息 key
*/
public static final String JWT_PAYLOAD_USER_KEY = "user";
/**
* 刷新token次数 默认为0起始
*/
public static final String REFRESH_TOKEN_NUMBER = "refreshTokenNumber";
/**
* access-token
*/
public static final String ACCESS_TOKEN = "access-token";
/**
* 加密token
*
* @param userInfo 载荷中的数据
* @param jwtConfig jwt 配置
* @return JWT
*/
public static String createAccessToken(Object userInfo, JwtConfig jwtConfig) {
Map map = new HashMap<>();
map.put(JWT_PAYLOAD_USER_KEY, userInfo);
map.put(REFRESH_TOKEN_NUMBER, 0);
return Jwts.builder()
.setClaims(map)
.setId(createJTI())
.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getAccessTokenExpire() * 1000))
.signWith(SignatureAlgorithm.HS256, jwtConfig.getAccessTokenSecret())
.compact();
}
/**
* 生成 RefreshToken
*
* @return refreshToken
*/
public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig) {
return createRefreshToken(userInfo, jwtConfig, 0);
}
/**
* 生成 RefreshToken
*
* @return refreshToken
*/
public static String createRefreshToken(Object userInfo, JwtConfig jwtConfig, int refreshTokenNumber) {
Map map = new HashMap<>();
map.put(JWT_PAYLOAD_USER_KEY, userInfo);
map.put(REFRESH_TOKEN_NUMBER, refreshTokenNumber);
return Jwts.builder()
.setClaims(map)
.setId(createJTI())
.setExpiration(new Date(System.currentTimeMillis() + jwtConfig.getRefreshTokenExpire() * 1000))
.signWith(SignatureAlgorithm.HS256, jwtConfig.getRefreshTokenSecret())
.compact();
}
/**
* 解析 refreshToken
*
* @param token token
* @param jwtConfig 配置项
* @return 载荷信息
*/
public static Claims parserAccessToken(String token, JwtConfig jwtConfig) {
return parserToken(token, jwtConfig.getAccessTokenSecret());
}
/**
* 解析 token
*
* @param token token
* @param jwtConfig 配置项
* @return 载荷信息
*/
public static Claims parserRefreshToken(String token, JwtConfig jwtConfig) {
return parserToken(token, jwtConfig.getRefreshTokenSecret());
}
/**
* 获取token中的载荷信息
*
* @param token 用户请求中的令牌
* @return 用户信息
*/
public static Claims parserToken(String token, String secretKey) {
try {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException e) {
log.error("token{}过期", token, e);
throw new JwtApiException(ResultCodeEnum.JWT_EXPIRED.code(), ResultCodeEnum.JWT_EXPIRED.message());
} catch (SignatureException e) {
log.error("token=[{}], 签名", token, e);
throw new JwtApiException(ResultCodeEnum.JWT_SIGNATURE.code(), ResultCodeEnum.JWT_SIGNATURE.message());
} catch (Exception e) {
log.error("token=[{}]解析错误 message:{}", token, e.getMessage(), e);
throw new JwtApiException(ResultCodeEnum.JWT_ERROR.code(), ResultCodeEnum.JWT_ERROR.message());
}
}
private static String createJTI() {
return new String(java.util.Base64.getEncoder().encode(UUID.randomUUID().toString().getBytes()));
}
}