- 环境:springboot 2.6.0-SNAPSHOT
参考大佬博客:https://blog.csdn.net/mengxianglong123/article/details/112463172
- 本文没有什么是JWT,什么是SpringSecurity不了解的自行学习,且本文不涉及OAuth2
- 案例属于前后端分离,且后端连接DB
application.yaml
server:
port: 9090
spring:
# jdbc
datasource:
username: root
password: 990415
url: jdbc:mysql://127.0.0.1:3306/my_blog?serverTimezone=UTC&characterEncoding=utf-8&useUnicode=true
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
mybatis-plus:
# pojo
type-aliases-package: top.jybill.demo.pojo
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: sm_
使用mybatis-plus便于开发
SpringSecurityConfig配置类
@Configuration
@EnableWebSecurity
// 开启注解的使用
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@ConditionalOnClass({
MyUserDetailsServiceImpl.class, // 登陆处理器
MyAccessDeniedHandler.class, // 登陆后出现权限异常的处理器
MyAuthenticationExceptionHandler.class, // 登录前未承认出现异常的处理器
MyAuthenticationSuccessHandler.class, // 登录成功处理器 (生成token)
MyForwardAuthenticationFailureHandler.class, // 登录失败处理器
MyJWTAuthenticationFilter.class // JWT校验的一个过滤器, 最先进行校验的过滤器 (校验token)
})
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
// 这一坨是加载依赖...
private final UserDetailsService myUserDetailsServiceImpl;
private final MyAccessDeniedHandler myAccessDeniedHandler;
private final MyAuthenticationExceptionHandler myAuthenticationExceptionHandler;
private final MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
private final MyForwardAuthenticationFailureHandler myForwardAuthenticationFailureHandler;
private final MyJWTAuthenticationFilter myJWTAuthenticationFilter;
@Autowired
public SpringSecurityConfig(UserDetailsService myUserDetailsServiceImpl,
MyAccessDeniedHandler myAccessDeniedHandler,
MyAuthenticationExceptionHandler myAuthenticationExceptionHandler,
MyAuthenticationSuccessHandler myAuthenticationSuccessHandler,
MyForwardAuthenticationFailureHandler myForwardAuthenticationFailureHandler,MyJWTAuthenticationFilter myJWTAuthenticationFilter) {
this.myUserDetailsServiceImpl = myUserDetailsServiceImpl;
this.myAccessDeniedHandler = myAccessDeniedHandler;
this.myAuthenticationExceptionHandler = myAuthenticationExceptionHandler;
this.myAuthenticationSuccessHandler = myAuthenticationSuccessHandler;
this.myForwardAuthenticationFailureHandler = myForwardAuthenticationFailureHandler;
this.myJWTAuthenticationFilter = myJWTAuthenticationFilter;
}
// 这一坨是加载依赖...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(this.myUserDetailsServiceImpl).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login/**", "/logout/**").permitAll()
.antMatchers("/t/in").authenticated()
.antMatchers("/t/inRole").authenticated()
.antMatchers("/t/out").permitAll()
.and()
.formLogin()
.successHandler(this.myAuthenticationSuccessHandler)
.failureHandler(this.myForwardAuthenticationFailureHandler)
.and()
.exceptionHandling().accessDeniedHandler(this.myAccessDeniedHandler) // 异常处理
.authenticationEntryPoint(this.myAuthenticationExceptionHandler)
.and()
.csrf().disable() // 关闭csrf跨域攻击
// 前后端分离JWT 关闭session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 前后端分离JWT过滤器
.and().addFilterBefore(this.myJWTAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.headers().cacheControl()// 关闭页面缓存
;
}
// 密码编码
@Bean
public PasswordEncoder getPasswordEncoder() {
return new BCryptPasswordEncoder();
}
// 授权管理器
@Bean
public AuthenticationManager getAuthenticationManager() throws Exception {
return super.authenticationManagerBean();
}
}
自定义service登陆类
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper; // 使用的mybatis-plus
@Autowired
public MyUserDetailsServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// mybatis-plus操作
QueryWrapper myUserQueryWrapper = new QueryWrapper<>();
myUserQueryWrapper.eq("account", username);
MyUser myUsers = userMapper.selectOne(myUserQueryWrapper); // vo类
List grantedAuthorities = null;
if (myUsers.getAdmin()) {
// springsecurity将字符转权限的方法
grantedAuthorities = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
}
// 返回查出来的用户, SpringSecurity定义的user类
return new User(myUsers.getAccount(),
new BCryptPasswordEncoder().encode(myUsers.getPassword()),
"1".equals(myUsers.getStatus()),
"1".equals(myUsers.getStatus()),
"1".equals(myUsers.getStatus()),
"1".equals(myUsers.getStatus()),
grantedAuthorities
);
}
}
登陆前异常处理类:没有登陆想访问需要登陆的接口
@Component
public class MyAuthenticationExceptionHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
HashMap ret = new HashMap<>();
// 未登录
if (e instanceof InsufficientAuthenticationException) {
ret.put("code", HttpServletResponse.SC_FORBIDDEN);
ret.put("msg", e.getMessage());
response.getWriter().write(JSON.toJSONString(ret));
}
}
}
登陆成功处理类:登陆成功生成token并生成springsecurity的安全上下文
@Component
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HashMap claims = new HashMap<>();
claims.put("username", authentication.getName());
List role = new ArrayList<>();
for (GrantedAuthority g : authentication.getAuthorities()) {
role.add(g.getAuthority());
}
claims.put("grantedAuthority", role);
String token = JJWTUtil.createToken(claims);
//将生成的authentication放入容器中,生成安全的上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
HashMap ret = new HashMap<>();
ret.put("token", token);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().println(JSON.toJSONString(ret));
}
}
我们后续需要拿着返回的JSON - token访问接口
登陆失败处理类:账号密码错误
@Component
public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler{
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8");
HashMap ret = new HashMap<>();
ret.put("code", 400);
ret.put("msg", "账号密码错误");
response.getWriter().println(JSON.toJSONString(ret));
}
}
JWT校验过滤器:校验token是否合法,是否过期
@Slf4j
@Component
public class MyJWTAuthenticationFilter extends OncePerRequestFilter {
// request headers 的key
private static final String TOKEN_PREFIX = "Authentication";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authentication = request.getHeader(TOKEN_PREFIX);
Claims claims = null;
// 校验JWT
if (StrUtil.isNotBlank(authentication)) {
claims = JJWTUtil.checkToken(authentication);
} else {
logger.warn("没有传JWT");
}
// once we get the token validate it.
log.info("security上下文: {}", SecurityContextHolder.getContext().getAuthentication());
if (claims != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String username = claims.get("username", String.class);
ArrayList grantedAuthority = (ArrayList)claims.get("grantedAuthority");
StringBuilder authorityString = new StringBuilder();
for (String s : grantedAuthority) {
authorityString.append(s);
authorityString.append(",");
}
String s = StrUtil.subBefore(authorityString.toString(), ",", true);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(username, null, AuthorityUtils.commaSeparatedStringToAuthorityList(s));
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); // 生成安全上下文
}
chain.doFilter(request, response);
}
}
token合法、为失效会生成安全上下文,否则交给springsecurity自己判断,肯定是失败的,就会走登陆后权限异常的处理器
权限异常处理器
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
HashMap map = new HashMap<>();
map.put("msg", accessDeniedException.getMessage());
map.put("code", 403);
System.out.println(map);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=utf-8");
response.getWriter().println(JSON.toJSONString(map));
}
}
controller + postman测试
@RestController
@RequestMapping("/t")
public class TestController {
@GetMapping("out")
public Map outSecurity() {
HashMap map = new HashMap<>();
map.put("msg", "out OK");
return map;
}
@GetMapping("in")
public Map inSecurity() {
HashMap map = new HashMap<>();
map.put("msg", "in OK");
return map;
}
@GetMapping("inAutority")
@PreAuthorize("hasAuthority('ban')")
public Map inAutority(Authentication authentication) {
HashMap map = new HashMap<>();
map.put("msg", "ban");
map.put("auth", authentication);
return map;
}
@GetMapping("notInAutority")
@PreAuthorize("hasAuthority('admin')")
public Map notInAutority(Authentication authentication) {
HashMap map = new HashMap<>();
map.put("msg", "admin");
map.put("auth", authentication);
return map;
}
}
- 登陆测试:
- 认证测试:
- 权限测试:
源码jwt本身的特性就是无状态,所以没必要后端在db保存
码云:https://gitee.com/JYbill/springseucirty-jjwt



