认证和授权学习6:前后端分离状态下使用springsecurity
本文使用的springboot版本是2.1.3.RELEASE
一、简要描述
默认情况下,spring security登录成功或失败,都会返回一个302跳转。登录成功跳转到主页,失败跳转到登录页。如果未认证直接访问受保护资源也会跳转到登录页 。
而在前后端分离项目中,前后端是通过json数据进行交互,前端通过ajax请求和后端进行交互,ajax是无法处理302跳转的,所以我们希望不管是未登录还是登录成功,spring security都给前端返回json数据,而前端自己根据返回结果进行逻辑控制。
springsecurity默认采用的是表单登录,而我们希望的登录流程是这样的:
(1) 前端带着用户名和密码用ajax请求登录,认证成功后返回一个token值给前端
(2) 下次请求时在请求头中携带这个token,后端校验这个token通过后放行请求,否则提示未登录(返回json数据)
二、配置让springsecurity返回 json数据
2.1 未登录时访问受限资源的处理
未登录时访问资源,请求会被FilterSecurityInterceptor
这个过滤器拦截到,然后抛出异常,这个异常会被
ExceptionTranslationFilter
这个过滤器捕获到,并最终交给AuthenticationEntryPoint
接口的commence方法处理。
所以处理办法是自定义一个AuthenticationEntryPoint
的实现类并配置到springsecurity中
/**
* 未登录时访问受限资源的处理方式
*/
public class UnLoginHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = mapper.createObjectNode();
if(authException instanceof BadCredentialsException){
//账号或密码错误
objectNode.put("code", "501");
objectNode.put("message", "账号或者密码错误");
}else {
objectNode.put("code", "500");
objectNode.put("message", "未登录或token无效");
}
response.setHeader("Content-Type", "application/json;charset=UTF-8");
response.getWriter().print(objectNode);
}
}
配置到spring security中,
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...省略其他配置
//安全配置
@Override
protected void configure(HttpSecurity http) throws Exception {
...省略其他配置
//设置未登录或登录失败时访问资源的处理方式
http.exceptionHandling().authenticationEntryPoint(new UnLoginHandler());
...
}
}
2.2 访问资源权限不足时的处理
当一个已登录用户访问了一个没有权限的资源时,springsecurity默认会重定向到一个403页面。可以通过自己实现 AccessDeniedHandler
接口然后配置到springsecurity中来自定义
/**
* 当前登录的用户没有权限访问资源时的处理器
*/
public class NoAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ObjectMapper mapper = new ObjectMapper();
ObjectNode objectNode = mapper.createObjectNode();
objectNode.put("code","500");
objectNode.put("message","访问失败,权限不够");
response.setHeader("Content-Type","application/json;charset=UTF-8");
response.getWriter().print(objectNode);
}
}
配置到springsecurity中
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...省略其他配置
//安全配置
@Override
protected void configure(HttpSecurity http) throws Exception {
...省略其他配置
//设置权限不足,无法访问当前资源时的处理方式
http.exceptionHandling().accessDeniedHandler(new NoAccessDeniedHandler());
...
}
}
这样配置后,未登录,登录失败,权限不足这些场景下springsecurity就会返回json数据给前端。
三、如何发token
这一节来解决发token的问题。现在已经去掉了表单登录的功能,那如何让springsecurity验证账号和密码并创建token呢。
可以自定义一个接口给前端请求,用来发token,前端提交账号和密码到这个接口,在其中调用springsecurity的认证管理器来认证账号密码,认证成功后创建一个token返回给前端
@RestController
@RequestMapping("/authenticate")
public class AuthenticationController {
private final static ObjectMapper MAPPER=new ObjectMapper();
//注入springsecurity的认证管理器
@Autowired
private AuthenticationManager authenticationManager;
/**
* 创建token
* @return
*/
@PostMapping("/applyToken")
public JsonNode applyToken(@RequestBody UserDto userDto){
ObjectNode tokenNode = MAPPER.createObjectNode();
//1.创建UsernamePasswordAuthenticationToken对象
UsernamePasswordAuthenticationToken authenticationToken=new UsernamePasswordAuthenticationToken(userDto.getUsername(),userDto.getPassword());
//2.交给认证管理器进行认证
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if(null!=authenticate){
//认证成功,生成token返回给前端
String token = JwtUtils.createToken(userDto.getUsername());
if(StringUtils.isEmpty(token)){
tokenNode.put("code","401");
tokenNode.put("message","生成token失败");
}else {
tokenNode.put("code","200");
tokenNode.put("token", token);
tokenNode.put("message","success");
}
tokenNode.put("code","200");
tokenNode.put("token", JwtUtils.createToken(userDto.getUsername()));
tokenNode.put("message","success");
return tokenNode;
}else{
tokenNode.put("code","401");
tokenNode.put("message","登录失败");
}
return tokenNode;
}
}
其中JwtUtils是一个自定义的jwt 工具类,提供了生成token和验证token的功能
四、如何让springsecurity验证token
上边实现了发token的功能,那如何让springsecurity验证这个token,并放行请求。可以自定义一个过滤器,在springsecurity的登录过滤器之前先拦截请求,然后进行token,如果验证通过了就把当前用户设置到SecurityContextHolder
中,这样就完成了验证和登录。
自定义过滤器
/**
* 验证请求携带的token是否有效
*/
@Component
public class TokenVerifyFilter extends GenericFilterBean {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest) servletRequest;
//从请求头中获取token
String token = request.getHeader("Authorization-Token");
if (StringUtils.hasText(token)) {
//从token中解析用户名
String username = JwtUtils.getUserInfo(token);
//查询当前用户
if(!StringUtils.isEmpty(username)){
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if(null!=userDetails){
//查询不到表示用户不存在
//从token中获取用户信息封装成 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(token, "", userDetails.getAuthorities());
//设置用户信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
}
} catch (Exception e) {
//登录发生异常,但要继续走其余过滤器的逻辑
e.printStackTrace();
}
//继续执行springsecurity的过滤器
filterChain.doFilter(servletRequest, servletResponse);
}
}
把这个过滤器设置到UsernamePasswordAuthenticationFilter
之前。
完整的springsecurity安全配置如下
/**
* 配置springsecurity
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//用户配置,
@Bean
public UserDetailsService userDetailsService(){
//在内存中配置用户
InMemoryUserDetailsManager manager=new InMemoryUserDetailsManager();
manager.createUser(User.withUsername("lyy").password("123").authorities("ROLE_P1").build());
manager.createUser(User.withUsername("zs").password("456").authorities("ROLE_P2").build());
return manager;
}
//配置自定义的对token进行验证的过滤器
@Autowired
private TokenVerifyFilter tokenVerifyFilter;
//密码加密方式配置
@Bean
public PasswordEncoder passwordEncoder(){
return NoOpPasswordEncoder.getInstance();
}
//安全配置
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
//匹配路径时越具体的路径要先匹配
http.authorizeRequests().antMatchers("/","/index.html").permitAll();
//放行申请token的url
http.authorizeRequests().antMatchers("/authenticate/**").permitAll();
//需要p1权限才能访问
http.authorizeRequests().antMatchers("/resource/r1").hasRole("P1");
//需要p2权限才能访问
http.authorizeRequests().antMatchers("/resource/r2").hasRole("P2")
.antMatchers("/resource/r3").hasRole("P3");//需要p3权限才能访问
http.authorizeRequests().anyRequest().authenticated();
http.formLogin().disable();//禁用表单登录
//设置未登录或登录失败时访问资源的处理方式
http.exceptionHandling().authenticationEntryPoint(new UnLoginHandler());
//设置权限不足,无法访问当前资源时的处理方式
http.exceptionHandling().accessDeniedHandler(new NoAccessDeniedHandler());
http.addFilterBefore(tokenVerifyFilter, UsernamePasswordAuthenticationFilter.class);
//设置不使用session,无状态
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
/**
* 配置认证管理器:
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
五、总结
按上边这样配置后,前端向先请求发token的接口获取一个token,然后在每次访问后端时都在请求头中带上这个token,后端验证了这个token后就会放行请求。
完整的示例工程:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!