一堆过滤器链的集合。

<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.0version>
dependency>
SpringBoot启动类上添加注解:@EnableWebSecurity
security:
user:
name: admin
password: admin
分为两种:
验证码过滤器:
因为前后端分离禁用session,所以把验证码存入到redis中。

验证码controller(可选)
@Slf4j
@RestController
public class AuthController extends BaseController{
@Autowired
private Producer producer;
/**
* 图片验证码
*/
@GetMapping("/captcha")
public Result captcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
String code = producer.createText();
String key = UUID.randomUUID().toString();
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
// 存储到redis中
redisUtil.hset(Const.captcha_KEY, key, code, 120);
log.info("验证码 -- {} - {}", key, code);
return Result.succ(
MapUtil.builder()
.put("token", key)
.put("base64Img", base64Img)
.build()
);
}
}
解决跨域(可选)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addExposedHeader("Authorization");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
// .allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600);
}
}
配置security总配置文件
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
//告诉security使用了哪种密码加密方式
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
//拦截白名单
private static final String[] URL_WHITELIST = {
"/login",
"/logout",
"/captcha",
"/favicon.ico",
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
// 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class) //先校验验证码
;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailService);
}
}
创建security包,配置过滤器链
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
// 生成jwt,并放置到请求头中
String jwt = jwtUtils.generateToken(authentication.getName());
response.setHeader(jwtUtils.getHeader(), jwt);
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail("用户名或密码错误");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
JWT验证过滤器
每次请求都会验证是否登录,token是否正确
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
UserDetailServiceImpl userDetailService;
@Autowired
SysUserService sysUserService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
//为空也放行,可能是静态资源
String jwt = request.getHeader(jwtUtils.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
SysUser sysUser = sysUserService.getByUsername(username);
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
JWT错误过滤器
用来统一JWT验证错误的返回结果,而不是返回一个默认登录页面,因为前后端分离项目不需要返回页面。
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail("请先登录");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
用户权限不足过滤器
用户若没有相应的访问权限则统一返回失败结果。
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
Result result = Result.fail(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
验证码过滤器
在校验登录账号密码前先校验验证码。
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
try{
// 校验验证码
validate(httpServletRequest);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("token");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误");
}
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
从数据库读取用户密码匹配
也都可放入security包中。
public class AccountUser implements UserDetails {
private Long userId;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public AccountUser(Long userId, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(userId, username, password, true, true, true, true, authorities);
}
public AccountUser(Long userId, String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.userId = userId;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
SysUserService sysUserService;
//从数据库加载封装对应用户名的正确的用户实体对象
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getByUsername(username);
if (sysUser == null) {
throw new UsernameNotFoundException("用户名或密码不正确");
}
return new AccountUser(sysUser.getId(), sysUser.getUsername(), sysUser.getPassword(), getUserAuthority(sysUser.getId()));
}
/**
* 获取用户权限信息(角色、菜单权限)
* @param userId
* @return
*/
public List<GrantedAuthority> getUserAuthority(Long userId){
// 角色(ROLE_admin)、菜单操作权限 sys:user:list
String authority = sysUserService.getUserAuthorityInfo(userId); // ROLE_admin,ROLE_normal,sys:user:list,....
//见下面代码
return AuthorityUtils.commaSeparatedStringToAuthorityList(authority);
}
}
其中getUserAuthorityInfo(userId)根据用户ID查出所有权限的字符串格式代码:
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
@Autowired
SysRoleService sysRoleService;
@Autowired
SysUserMapper sysUserMapper;
@Autowired
SysMenuService sysMenuService;
@Autowired
RedisUtil redisUtil;
@Override
public SysUser getByUsername(String username) {
return getOne(new QueryWrapper<SysUser>().eq("username", username));
}
@Override
public String getUserAuthorityInfo(Long userId) {
SysUser sysUser = sysUserMapper.selectById(userId);
// ROLE_admin,ROLE_normal,sys:user:list,....
String authority = "";
//如果redis缓存了就从redis中取,其实这里存入userID要比username在删除缓存时更简单点
if (redisUtil.hasKey("GrantedAuthority:" + sysUser.getUsername())) {
authority = (String) redisUtil.get("GrantedAuthority:" + sysUser.getUsername());
} else {
// 获取角色编码
List<SysRole> roles = sysRoleService.list(new QueryWrapper<SysRole>()
.inSql("id", "select role_id from sys_user_role where user_id = " + userId));
if (roles.size() > 0) {
String roleCodes = roles.stream().map(r -> "ROLE_" + r.getCode()).collect(Collectors.joining(","));
authority = roleCodes.concat(",");
}
// 获取菜单操作编码
List<Long> menuIds = sysUserMapper.getNavMenuIds(userId);
if (menuIds.size() > 0) {
List<SysMenu> menus = sysMenuService.listByIds(menuIds);
String menuPerms = menus.stream().map(m -> m.getPerms()).collect(Collectors.joining(","));
authority = authority.concat(menuPerms);
}
redisUtil.set("GrantedAuthority:" + sysUser.getUsername(), authority, 60 * 60);
}
return authority;
}
//发生变动时删除redis缓存
@Override
public void clearUserAuthorityInfo(String username) {//用户权限变了
redisUtil.del("GrantedAuthority:" + username);
}
@Override
public void clearUserAuthorityInfoByRoleId(Long roleId) {//角色名等变了
List<SysUser> sysUsers = this.list(new QueryWrapper<SysUser>()
.inSql("id", "select user_id from sys_user_role where role_id = " + roleId));
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
@Override
public void clearUserAuthorityInfoByMenuId(Long menuId) {//权限名等变了
List<SysUser> sysUsers = sysUserMapper.listByMenuId(menuId);
sysUsers.forEach(u -> {
this.clearUserAuthorityInfo(u.getUsername());
});
}
}
表结构:

需要设置两处权限:
第一次登陆的UserDetailServiceImpl里
登陆后调用接口的JWT验证过滤器里
使用注解在接口上标注需要的权限:
比如需要Admin角色权限:
@PreAuthorize(“hasRole(‘admin’)”)
比如需要添加用户的操作权限
@PreAuthorize(“hasAuthority(‘sys:user:save’)”)
退出登录处理器,放在security包中。并在security总配置类中指定该处理器即可。
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader(jwtUtils.getHeader(), "");
Result result = Result.succ("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
注意:
默认的登录请求url是"/login",并且只允许POST方式的请求。
在Controller中获取当前登录用户信息可以在参数列表中接收Principal对象。
UserDetailsService 返回给security封装的User对象进行用户名密码自动校验。
PasswordEncoder 同样的密码每次加密的密文也不一样。
由Json对象组成的web令牌,替代传统登录保存到服务器内存的session的方式。
由于JWT基于客户端所以重启服务器JWT也不会失效。
1,授权
主流用法,一旦用户登录,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务,资源。单点登录就是JWT的一项功能,因为它开销很小且跨域轻松。
2,信息交换
JWT是在各方之间安全传输信息的好方法,因为可以对JWT进行签名(使用公钥/私钥对),所以你可以确保发件人是他们所说的,此外你还可以验证内容是否遭到篡改。
前端将用户名密码发送给后端,后端核对成功后根据用户信息生成一个JWT(Token),后端将JWT作为登陆成功返回结果给前端,前端保存在本地localStorage或sessionStorage上(退出登录时前端删除保存的JWT即可),前端之后每次请求时将JWT放入HTTP Header中,后端校验JWT有效性,如签名是否正确、Token是否过期、检查Token的接收方是否是自己等,后端验证成功后使用JWT中的用户信息完成相关请求操作。
标头.有效载荷.签名

通过Base64编码成字符串

注意:由于Base64编码是可逆的,请勿在JWT中存放敏感信息。
1.引入依赖
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
2,生成token
HashMap<String,Object> map =new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND, 90);
//生成令牌
String token = JWT.create()
.WithHeader(map) //header,可省略使用默认
.withClaim("userId", 12) //设置自定义用户名
.withClaim("username", "张三")
.withExpiresAt(instance.getTime()) //设置过期时间
.sign(Algorithm.HMAC256("!Q2W#E$RW")); //设置签名 保密 复杂
//输出令牌
System.out.println(token);
3.验证token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("!Q2W#E$RW")).build();
//验证失败会抛异常
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//存的是时候是什么类型,取得时候就是什么类型,否则取不到值。
System.out.println("用户名: " + decodedJWT.getClaim("userId").asInt());
System.out.println("用户名: " + decodedJWT.getClaim("username").asString());
System.out.println("过期时间: "+decodedJWT.getExpiresAt());
4,使用JWT工具类
public class JWTUtils {
private static String SECRET_KEY = "1123vbnmASDLQWERTYUIOZXCVBNM";
//根据用户信息生成token
public static String createToken(CurrentUser currentUser) {
Algorithm algorithm = Algorithm.HMAC256(SECRET_KEY);
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 30);
String token = JWT.create()
.withClaim("username", currentUser.getUsername())
.withExpiresAt(calendar.getTime())
.sign(algorithm);
return token;
}
//校检 token 是否过期
private static DecodedJWT verifyToken(String token) {
return JWT.require(Algorithm.HMAC256(SECRET_KEY)).build().verify(token);
}
//根据 token 解析出用户名
public static String parseUserInfo(String token) {
DecodedJWT decodedJWT = verifyToken(token);
return decodedJWT.getClaim("username").asString();
}
}
由于很多接口都需要验证JWT,单体应用可以使用拦截器,分布式微服务可以使用网关优化。