写点什么

🍃【Spring 专题】「开发实战」Spring Security 与 JWT 实现权限管控以及登录认证指南

作者:浩宇天尚
  • 2021 年 12 月 26 日
  • 本文字数:13885 字

    阅读完需:约 46 分钟

🍃【Spring专题】「开发实战」Spring Security与JWT实现权限管控以及登录认证指南

SpringSecurity 介绍

SpringSecurity 是一个用于 Java 企业级应用程序的安全框架,主要包含用户认证和用户授权两个方面,相比较 Shiro 而言,Security 功能更加的强大,它可以很容易地扩展以满足更多安全控制方面的需求,但也相对它的学习成本会更高,两种框架各有利弊。实际开发中还是要根据业务和项目的需求来决定使用哪一种.

JWT 的认证传输协议

JWT 是在 Web 应用中安全传递信息的规范,从本质上来说是 Token 的演变,是一种生成加密用户身份信息的 Token,特别适用于分布式单点登陆的场景,无需在服务端保存用户的认证信息,而是直接对 Token 进行校验获取用户信息,使单点登录更为简单灵活。

系统搭建

环境管控

  • SpringBoot 版本:2.1.6

  • SpringSecurity 版本: 5.1.5

  • MyBatis-Plus 版本: 3.1.0

  • JDK 版本:1.8

Maven 依赖如下:

<dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <scope>runtime</scope>        </dependency>        <dependency>            <groupId>org.projectlombok</groupId>            <artifactId>lombok</artifactId>            <optional>true</optional>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <!--Security依赖 -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-security</artifactId>        </dependency>        <!-- MybatisPlus 核心库 -->        <dependency>            <groupId>com.baomidou</groupId>            <artifactId>mybatis-plus-boot-starter</artifactId>            <version>3.1.0</version>        </dependency>        <!-- 引入阿里数据库连接池 -->        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>druid</artifactId>            <version>1.1.6</version>        </dependency>        <!-- StringUtilS工具 -->        <dependency>            <groupId>org.apache.commons</groupId>            <artifactId>commons-lang3</artifactId>            <version>3.5</version>        </dependency>        <!-- JSON工具 -->        <dependency>            <groupId>com.alibaba</groupId>            <artifactId>fastjson</artifactId>            <version>1.2.45</version>        </dependency>        <!-- JWT依赖 -->        <dependency>            <groupId>org.springframework.security</groupId>            <artifactId>spring-security-jwt</artifactId>            <version>1.0.9.RELEASE</version>        </dependency>        <dependency>            <groupId>io.jsonwebtoken</groupId>            <artifactId>jjwt</artifactId>            <version>0.9.0</version>        </dependency></dependencies>
复制代码

springboot 配置文件

配置如下:
# 配置端口server:  port: 8888spring:  # 配置数据源  datasource:    driver-class-name: com.mysql.cj.jdbc.Driver    url: jdbc:mysql://localhost:3306/call_center?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false    username: root    password: root    type: com.alibaba.druid.pool.DruidDataSource# JWT配置jwt:  # 密匙KEY  secret: JWTSecret  # HeaderKEY  tokenHeader: Authorization  # Token前缀字符  tokenPrefix: callcenter-  # 过期时间 单位秒 1天后过期=86400 7天后过期=604800  expiration: 86400  # 配置不需要认证的接口  antMatchers: /index,/login/**,/favicon.ico# Mybatis-plus相关配置mybatis-plus:  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)  mapper-locations: classpath:mapper/*.xml  # 以下配置均有默认值,可以不设置  global-config:    db-config:      #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";      id-type: AUTO      #字段策略 IGNORED:"忽略判断"  NOT_NULL:"非 NULL 判断")  NOT_EMPTY:"非空判断"      field-strategy: NOT_EMPTY      #数据库类型      db-type: MYSQL  configuration:    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射    map-underscore-to-camel-case: true    # 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏    call-setters-on-nulls: true    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
复制代码

开发核心类

编写 JWT 工具类

@Slf4jpublic class JWTTokenUtil {
/** * 生成Token * @Param selfUserEntity 用户安全实体 * @Return Token */ public static String createAccessToken(SelfUserEntity selfUserEntity){ // 登陆成功生成JWT String token = Jwts.builder() // 放入用户名和用户ID .setId(selfUserEntity.getUserId()+"") // 主题 .setSubject(selfUserEntity.getUsername()) // 签发时间 .setIssuedAt(new Date()) // 签发者 .setIssuer("sans") // 自定义属性 放入用户拥有权限 .claim("authorities", JSON.toJSONString(selfUserEntity.getAuthorities())) // 失效时间 .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 签名算法和密钥 .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) .compact(); return token; }}
复制代码

无权限处理类

@Componentpublic class UserAuthAccessDeniedHandler implements AccessDeniedHandler{    /**     * 暂无权限返回结果     */    @Override    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){        ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授权"));    }}
复制代码

用户未登录处理类

/** * 用户未登录处理类 */@Componentpublic class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint{    @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){        ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登录"));    }}
复制代码

登录失败处理类

@Slf4j@Componentpublic class UserLoginFailureHandler implements AuthenticationFailureHandler {    /**     * 登录失败返回结果     * @Author Sans     * @CreateTime 2019/10/3 9:12     */    @Override    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){        // 这些对于操作的处理类可以根据不同异常进行不同处理        if (exception instanceof UsernameNotFoundException){            log.info("【登录失败】"+exception.getMessage());            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名不存在"));        }        if (exception instanceof LockedException){            log.info("【登录失败】"+exception.getMessage());            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户被冻结"));        }        if (exception instanceof BadCredentialsException){            log.info("【登录失败】"+exception.getMessage());            ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名密码不正确"));        }        ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登录失败"));    }}
复制代码

登录成功处理类

@Slf4j@Componentpublic class UserLoginSuccessHandler implements AuthenticationSuccessHandler {    @Override    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){        // 组装JWT        SelfUserEntity selfUserEntity =  (SelfUserEntity) authentication.getPrincipal();        String token = JWTTokenUtil.createAccessToken(selfUserEntity);        token = JWTConfig.tokenPrefix + token;        // 封装返回参数        Map<String,Object> resultData = new HashMap<>();        resultData.put("code","200");        resultData.put("msg", "登录成功");        resultData.put("token",token);        ResultUtil.responseJson(response,resultData);    }}
复制代码

登出成功处理类

@Componentpublic class UserLogoutSuccessHandler implements LogoutSuccessHandler {    @Override    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){        Map<String,Object> resultData = new HashMap<>();        resultData.put("code","200");        resultData.put("msg", "登出成功");        SecurityContextHolder.clearContext();        ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));    }}
复制代码

编写 Security 核心类

自定义登录验证
@Componentpublic class UserAuthenticationProvider implements AuthenticationProvider {    @Autowired    private SelfUserDetailsService selfUserDetailsService;    @Autowired    private SysUserService sysUserService;    @Override    public Authentication authenticate(Authentication authentication) throws AuthenticationException {        // 获取表单输入中返回的用户名        String userName = (String) authentication.getPrincipal();        // 获取表单中输入的密码        String password = (String) authentication.getCredentials();        // 查询用户是否存在        SelfUserEntity userInfo = selfUserDetailsService.loadUserByUsername(userName);        if (userInfo == null) {            throw new UsernameNotFoundException("用户名不存在");        }        // 还要判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的        if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {            throw new BadCredentialsException("密码不正确");        }        // 还可以加一些其他信息的判断,比如用户账号已停用等判断        if (userInfo.getStatus().equals("PROHIBIT")){            throw new LockedException("该用户已被冻结");        }        // 角色集合        Set<GrantedAuthority> authorities = new HashSet<>();        // 查询用户角色        List<SysRoleEntity> sysRoleEntityList = sysUserService.selectSysRoleByUserId(userInfo.getUserId());        for (SysRoleEntity sysRoleEntity: sysRoleEntityList){            authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getRoleName()));        }        userInfo.setAuthorities(authorities);        // 进行登录        return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);    }    @Override    public boolean supports(Class<?> authentication) {        return true;    }}
复制代码
自定义 PermissionEvaluator 注解验证

自定义权限注解验证


@Componentpublic class UserPermissionEvaluator implements PermissionEvaluator {    @Autowired    private SysUserService sysUserService;    /**     * hasPermission鉴权方法     * 这里仅仅判断PreAuthorize注解中的权限表达式     * 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权     * @Param  authentication  用户身份     * @Param  targetUrl  请求路径     * @Param  permission 请求路径权限     * @Return boolean 是否通过     */    @Override    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {        // 获取用户信息        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();        // 查询用户权限(这里可以将权限放入缓存中提升效率)        Set<String> permissions = new HashSet<>();        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {            permissions.add(sysMenuEntity.getPermission());        }        // 权限对比        if (permissions.contains(permission.toString())){            return true;        }        return false;    }    @Override    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {        return false;    }}
复制代码

SpringSecurity 核心配置类

/** * SpringSecurity核心配置类 */@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true) //开启权限注解,默认是关闭的public class SecurityConfig extends WebSecurityConfigurerAdapter {    /**     * 自定义登录成功处理器     */    @Autowired    private UserLoginSuccessHandler userLoginSuccessHandler;    /**     * 自定义登录失败处理器     */    @Autowired    private UserLoginFailureHandler userLoginFailureHandler;    /**     * 自定义注销成功处理器     */    @Autowired    private UserLogoutSuccessHandler userLogoutSuccessHandler;    /**     * 自定义暂无权限处理器     */    @Autowired    private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;    /**     * 自定义未登录的处理器     */    @Autowired    private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;    /**     * 自定义登录逻辑验证器     */    @Autowired    private UserAuthenticationProvider userAuthenticationProvider;        /**     * 加密方式     * @CreateTime 2019/10/1 14:00     */    @Bean    public BCryptPasswordEncoder bCryptPasswordEncoder(){        return new BCryptPasswordEncoder();    }    /**     * 注入自定义PermissionEvaluator     */    @Bean    public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();        handler.setPermissionEvaluator(new UserPermissionEvaluator());        return handler;    }    /**     * 配置登录验证逻辑     */    @Override    protected void configure(AuthenticationManagerBuilder auth){        //这里可启用我们自己的登陆验证逻辑        auth.authenticationProvider(userAuthenticationProvider);    }    /**     * 配置security的控制逻辑     * @Author Sans     * @CreateTime 2019/10/1 16:56     * @Param  http 请求     */    @Override    protected void configure(HttpSecurity http) throws Exception {        http.authorizeRequests()                //不进行权限验证的请求或资源(从配置文件中读取)               .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()                //其他的需要登陆后才能访问                .anyRequest().authenticated()                .and()                //配置未登录自定义处理类                .httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)                .and()                //配置登录地址                .formLogin()                .loginProcessingUrl("/login/userLogin")                //配置登录成功自定义处理类                .successHandler(userLoginSuccessHandler)                //配置登录失败自定义处理类                .failureHandler(userLoginFailureHandler)                .and()                //配置登出地址                .logout()                .logoutUrl("/login/userLogout")                //配置用户登出自定义处理类                .logoutSuccessHandler(userLogoutSuccessHandler)                .and()                //配置没有权限自定义处理类                .exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)                .and()                // 取消跨站请求伪造防护                .csrf().disable();        // 基于Token不需要session        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);        // 禁用缓存        http.headers().cacheControl();        // 添加JWT过滤器        http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));    }}
复制代码

编写 JWT 拦截类

编写 JWT 接口请求校验拦截器
/** * JWT接口请求校验拦截器 * 请求接口时会进入这里验证Token是否合法和过期 */@Slf4jpublic class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {
public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) { super(authenticationManager);}
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 获得TokenHeader String tokenHeader = request.getHeader(JWTConfig.tokenHeader); if (null!=tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) { try { // 获取请求头中JWT的Token if (!StringUtils.isEmpty(request.getHeader(JWTConfig.tokenHeader))) { // 截取JWT前缀 String token = request.getHeader(JWTConfig.tokenHeader).replace(JWTConfig.tokenPrefix, ""); // 解析JWT Claims claims = Jwts.parser() .setSigningKey(JWTConfig.secret) .parseClaimsJws(token) .getBody(); // 获取用户名 String username = claims.getSubject(); String userId=claims.getId(); if(!StringUtils.isEmpty(username)&&!StringUtils.isEmpty(userId)) { // 获取角色 List<GrantedAuthority> authorities = new ArrayList<>(); String authority = claims.get("authorities").toString(); if(!StringUtils.isEmpty(authority)){ List<Map<String,String>> authorityMap = JSONObject.parseObject(authority, List.class); for(Map<String,String> role : authorityMap){ if(!StringUtils.isEmpty(role)) { authorities.add(new SimpleGrantedAuthority(role.get("authority"))); } } } //组装参数 SelfUserEntity selfUserEntity = new SelfUserEntity(); selfUserEntity.setUsername(claims.getSubject()); selfUserEntity.setUserId(Long.parseLong(claims.getId())); selfUserEntity.setAuthorities(authorities); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } } } catch (ExpiredJwtException e){ log.info("Token过期"); } catch (Exception e) { log.info("Token无效"); } } filterChain.doFilter(request, response); return; }}
复制代码

权限注解和 hasPermission 权限扩展

Security 允许我们在定义 URL 方法访问所应有的注解权限时使用 SpringEL 表达式,在定义所需的访问权限时如果对应的表达式返回结果为 true,则表示拥有对应的权限,反之则没有权限,会进入到我们配置的 UserAuthAccessDeniedHandler(暂无权限处理类)中进行处理.这里举一些例子,代码中注释有对应的描述.



    /**     * 管理端信息     * @Return Map<String,Object> 返回数据MAP     */    @PreAuthorize("hasRole('ADMIN')")    @RequestMapping(value = "/info",method = RequestMethod.GET)    public Map<String,Object> userLogin(){        Map<String,Object> result = new HashMap<>();        SelfUserEntity userDetails = SecurityUtil.getUserInfo();        result.put("title","管理端信息");        result.put("data",userDetails);        return ResultUtil.resultSuccess(result);    }    /**     * 拥有ADMIN或者USER角色可以访问     * @Return Map<String,Object> 返回数据MAP     */    @PreAuthorize("hasAnyRole('ADMIN','USER')")    @RequestMapping(value = "/list",method = RequestMethod.GET)    public Map<String,Object> list(){        Map<String,Object> result = new HashMap<>();        List<SysUserEntity> sysUserEntityList = sysUserService.list();        result.put("title","拥有用户或者管理员角色都可以查看");        result.put("data",sysUserEntityList);        return ResultUtil.resultSuccess(result);    }    /**     * 拥有ADMIN和USER角色可以访问     * @Return Map<String,Object> 返回数据MAP     */    @PreAuthorize("hasRole('ADMIN') and hasRole('USER')")    @RequestMapping(value = "/menuList",method = RequestMethod.GET)    public Map<String,Object> menuList(){        Map<String,Object> result = new HashMap<>();        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();        result.put("title","拥有用户和管理员角色都可以查看");        result.put("data",sysMenuEntityList);        return ResultUtil.resultSuccess(result);    }
复制代码


通常情况下使用 hasRole 和 hasAnyRole 基本可以满足大部分鉴权需求,但是有时候面对更复杂的场景上述常规表示式无法完成权限认证,Security 也为我们提供了解决方案。通过 hasPermission()来扩展表达式,使用 hasPermission(),首先要实现 PermissionEvaluator 接口

自定义权限注解验证

@Componentpublic class UserPermissionEvaluator implements PermissionEvaluator {    @Autowired    private SysUserService sysUserService;    /**     * hasPermission鉴权方法     * 这里仅仅判断PreAuthorize注解中的权限表达式     * 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权     * 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计     * @Param  authentication  用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)     * @Param  targetUrl  请求路径     * @Param  permission 请求路径权限     * @Return boolean 是否通过     */    @Override    public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {        // 获取用户信息        SelfUserEntity selfUserEntity =(SelfUserEntity) authentication.getPrincipal();        // 查询用户权限(这里可以将权限放入缓存中提升效率)        Set<String> permissions = new HashSet<>();        List<SysMenuEntity> sysMenuEntityList = sysUserService.selectSysMenuByUserId(selfUserEntity.getUserId());        for (SysMenuEntity sysMenuEntity:sysMenuEntityList) {            permissions.add(sysMenuEntity.getPermission());        }        // 权限对比        if (permissions.contains(permission.toString())){            return true;        }        return false;    }    @Override    public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {        return false;    }}
复制代码

请求方法上添加 hasPermission 示例

    /**     * 拥有sys:user:info权限可以访问     * hasPermission 第一个参数是请求路径 第二个参数是权限表达式     * @Return Map<String,Object> 返回数据MAP     */    @PreAuthorize("hasPermission('/admin/userList','sys:user:info')")    @RequestMapping(value = "/userList",method = RequestMethod.GET)    public Map<String,Object> userList(){        Map<String,Object> result = new HashMap<>();        List<SysUserEntity> sysUserEntityList = sysUserService.list();        result.put("title","拥有sys:user:info权限都可以查看");        result.put("data",sysUserEntityList);        return ResultUtil.resultSuccess(result);    }
复制代码

hasPermission 可以也可以和其他表达式联合使用

    /**     * 拥有ADMIN角色和sys:role:info权限可以访问     * @Author Sans     * @CreateTime 2019/10/2 14:22     * @Return Map<String,Object> 返回数据MAP     */    @PreAuthorize("hasRole('ADMIN') and hasPermission('/admin/adminRoleList','sys:role:info')")    @RequestMapping(value = "/adminRoleList",method = RequestMethod.GET)    public Map<String,Object> adminRoleList(){        Map<String,Object> result = new HashMap<>();        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();        result.put("title","拥有ADMIN角色和sys:role:info权限可以访问");        result.put("data",sysRoleEntityList);        return ResultUtil.resultSuccess(result);    }
复制代码

测试

创建账户这里用户加密使用了 Security 推荐的 bCryptPasswordEncoder 方法

注册用户操作

    @Test    public void contextLoads() {        // 注册用户        SysUserEntity sysUserEntity = new SysUserEntity();        sysUserEntity.setUsername("sans");        sysUserEntity.setPassword(bCryptPasswordEncoder.encode("123456"));        // 设置用户状态        sysUserEntity.setStatus("NORMAL");        sysUserService.save(sysUserEntity);        // 分配角色 1:ADMIN 2:USER        SysUserRoleEntity sysUserRoleEntity = new SysUserRoleEntity();        sysUserRoleEntity.setRoleId(2L);        sysUserRoleEntity.setUserId(sysUserEntity.getUserId());        sysUserRoleService.save(sysUserRoleEntity);    }
复制代码


登录 USER 角色账号,登录成功后我们会获取到身份认证的 Token。

登录用户操作


访问 USER 角色的接口,把上一步获取到的 Token 设置在 Headers 中,Key 为 Authorization,我们之前实现的 JWTAuthenticationTokenFilter 拦截器会根据请求头中的 Authorization 获取并解析 Token。

查看用户操作



使用 USER 角色 Token 访问 ADMIN 角色的接口,会被拒绝,告知未授权(暂无权限会进入我们定义的 UserAuthAccessDeniedHandler 这个类进行处理)


更换 ADMIN 角色进行登录并访问 ADMIN 接口

查看用户操作

非主线篇

本文讲述一下如何自定义 Spring Security 的登录认证操作,目前大多数的项目都是基于前后端分离的,但是也有很多场景下也会存在后端模板技术,如何给出一个采用 ajax 的登录及返回的前后端分离方式并且兼容后端模板技术的功能代码。

AjaxAuthSuccessHandler

public class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {    @Override    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {        response.setStatus(HttpServletResponse.SC_OK);    }}
复制代码

AjaxAuthFailHandler

public class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {    @Override    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication failed");    }}
复制代码

ajax 的异常处理

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {    @Override    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {        if(isAjaxRequest(request)){           response.sendError(HttpServletResponse.SC_UNAUTHORIZED,authException.getMessage());        }else{            response.sendRedirect("/login.html");        }
}
public static boolean isAjaxRequest(HttpServletRequest request) { String ajaxFlag = request.getHeader("X-Requested-With"); return ajaxFlag != null && "XMLHttpRequest".equals(ajaxFlag); }}
复制代码


这里我们自定义成功及失败的 ajax 返回,当然这里我们简单处理,只返回 statusCode

security 配置

@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint()) .and() .csrf().disable() .authorizeRequests() .antMatchers("/login","/css/**", "/js/**","/fonts/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login.html") .loginProcessingUrl("/login") .usernameParameter("name") .passwordParameter("password") .successHandler(new AjaxAuthSuccessHandler()) .failureHandler(new AjaxAuthFailHandler()) .permitAll() .and() .logout() .logoutUrl("/logout") .permitAll(); }
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("admin").password("admin").roles("USER"); }}
复制代码

这里有几个要注意的点:

  • permitAll:这里要添加前端资源路径,以及登陆表单请求的接口地址/login

  • loginPage:这里设置登录页面的地址,这里我们用静态页面,即 static 目录下的 login.html

  • ajax 配置:将 authenticationEntryPoint,successHandler,failureHandler 设置为上面自定义的 ajax 处理类

资料参考

https://segmentfault.com/a/1190000012140889


https://segmentfault.com/a/1190000010672041

发布于: 3 小时前
用户头像

浩宇天尚

关注

🏆 InfoQ写作平台-签约作者 🏆 2020.03.25 加入

【个人简介】酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“ 【技术格言】任何足够先进的技术都与魔法无异 【技术范畴】Java领域、Spring生态、MySQL专项、APM专题及微服务/分布式体系等

评论

发布
暂无评论
🍃【Spring专题】「开发实战」Spring Security与JWT实现权限管控以及登录认证指南