SpringBoot+SpringSecurity处理Ajax登陆请求

最近在项目中遇到了这样一个问题:先后端分离,前端用Vue来作,全部的数据请求都使用vue-resource,没有使用表单,所以数据交互都是使用JSON,后台使用Spring Boot,权限验证使用了Spring Security,由于以前用Spring Security都是处理页面的,此次单纯处理Ajax请求,所以记录下遇到的一些问题。这里的解决方案不只适用于Ajax请求,也能够解决移动端请求验证。css

建立工程

首先咱们须要建立一个Spring Boot工程,建立时须要引入Web、Spring Security、MySQL和MyBatis(数据库框架其实随意,我这里使用MyBatis),建立好以后,依赖文件以下:前端

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.1</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.11</version>
</dependency>

注意最后一个commons-codec依赖是我手动加入进来的,这是一个Apache的开源项目,能够用来生成MD5消息摘要,我在后文中将对密码进行简单的处理。vue

建立数据库并配置

为了简化逻辑,我这里建立了三个表,分别是用户表、角色表、用户角色关联表,以下: java

图片描述

接下来咱们须要在application.properties中对本身的数据库进行简单的配置,这里各位小伙伴视本身的具体状况而定。mysql

spring.datasource.url=jdbc:mysql:///vueblog
spring.datasource.username=root
spring.datasource.password=123

构造实体类

这里主要是指构造用户类,这里的用户类比较特殊,必须实现UserDetails接口,以下:web

public class User implements UserDetails {
    private Long id;
    private String username;
    private String password;
    private String nickname;
    private boolean enabled;
    private List<Role> roles;

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    @Override
    public List<GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : roles) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
        }
        return authorities;
    }
    //getter/setter省略...
}

实现了UserDetails接口以后,该接口中有几个方法须要咱们实现,四个返回Boolean的方法都是见名知意,enabled表示档期帐户是否启用,这个我数据库中确实有该字段,所以根据查询结果返回,其余的为了简单期间都直接返回true,getAuthorities方法返回当前用户的角色信息,用户的角色其实就是roles中的数据,将roles中的数据转换为List<GrantedAuthority>以后返回便可,这里有一个要注意的地方,因为我在数据库中存储的角色名都是诸如‘超级管理员’、‘普通用户’之类的,并非以ROLE_这样的字符开始的,所以须要在这里手动加上ROLE_,切记spring

另外还有一个Role实体类,比较简单,按照数据库的字段建立便可,这里再也不赘述。sql

建立UserService

这里的UserService也比较特殊,须要实现UserDetailsService接口,以下:数据库

@Service
public class UserService implements UserDetailsService {
    @Autowired
    UserMapper userMapper;
    @Autowired
    RolesMapper rolesMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userMapper.loadUserByUsername(s);
        if (user == null) {
            //避免返回null,这里返回一个不含有任何值的User对象,在后期的密码比对过程当中同样会验证失败
            return new User();
        }
        //查询用户的角色信息,并返回存入user中
        List<Role> roles = rolesMapper.getRolesByUid(user.getId());
        user.setRoles(roles);
        return user;
    }
}

实现了UserDetailsService接口以后,咱们须要实现该接口中的loadUserByUsername方法,即根据用户名查询用户。这里注入了两个MyBatis中的Mapper,UserMapper用来查询用户,RolesMapper用来查询角色。在loadUserByUsername方法中,首先根据传入的参数(参数就是用户登陆时输入的用户名)去查询用户,若是查到的用户为null,能够直接抛一个UsernameNotFoundException异常,可是我为了处理方便,返回了一个没有任何值的User对象,这样在后面的密码比对过程当中同样会发现登陆失败的(这里你们根据本身的业务需求调整便可),若是查到的用户不为null,此时咱们根据查到的用户id再去查询该用户的角色,并将查询结果放入到user对象中,这个查询结果将在user对象的getAuthorities方法中用上。json

Security配置

咱们先来看一下个人Security配置,而后我再来一一解释:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    UserService userService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
            }

            /**
             * @param charSequence 明文
             * @param s 密文
             * @return
             */
            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
            }
        });
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("超级管理员")
                .anyRequest().authenticated()//其余的路径都是登陆后便可访问
                .and().formLogin().loginPage("/login_page").successHandler(new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setContentType("application/json;charset=utf-8");
                PrintWriter out = httpServletResponse.getWriter();
                out.write("{\"status\":\"ok\",\"msg\":\"登陆成功\"}");
                out.flush();
                out.close();
            }
        })
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        out.write("{\"status\":\"error\",\"msg\":\"登陆失败\"}");
                        out.flush();
                        out.close();
                    }
                }).loginProcessingUrl("/login")
                .usernameParameter("username").passwordParameter("password").permitAll()
                .and().logout().permitAll().and().csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/reg");
    }
}

这是咱们配置的核心,小伙伴们听我一一道来:

1.首先这是一个配置类,所以记得加上@Configuration注解,又由于这是Spring Security的配置,所以记得继承WebSecurityConfigurerAdapter。
2.将刚刚建立好的UserService注入进来,一会咱们要用。
3.configure(AuthenticationManagerBuilder auth)方法中用来配置咱们的认证方式,在auth.userDetailsService()方法中传入userService,这样userService中的loadUserByUsername方法在用户登陆时将会被自动调用。后面的passwordEncoder是可选项,可写可不写,由于我是将用户的明文密码生成了MD5消息摘要后存入数据库的,所以在登陆时也须要对明文密码进行处理,因此就加上了passwordEncoder,加上passwordEncoder后,直接new一个PasswordEncoder匿名内部类便可,这里有两个方法要实现,看名字就知道方法的含义,第一个方法encode显然是对明文进行加密,这里我使用了MD5消息摘要,具体的实现方法是由commons-codec依赖提供的;第二个方法matches是密码的比对,两个参数,第一个参数是明文密码,第二个是密文,这里只须要对明文加密后和密文比较便可(小伙伴若是对此感兴趣能够继续考虑密码加盐)。
4.configure(HttpSecurity http)用来配置咱们的认证规则等,authorizeRequests方法表示开启了认证规则配置,antMatchers("/admin/**").hasRole("超级管理员")表示/admin/**的路径须要有‘超级管理员’角色的用户才能访问,我在网上看到小伙伴对hasRole方法中要不要加ROLE_前缀有疑问,这里是不要加的,若是用hasAuthority方法才须要加。anyRequest().authenticated()表示其余全部路径都是须要认证/登陆后才能访问。接下来咱们配置了登陆页面为login_page,登陆处理路径为/login,登陆用户名为username,密码为password,并配置了这些路径均可以直接访问,注销登录也能够直接访问,最后关闭csrf。在successHandler中,使用response返回登陆成功的json便可,切记不可使用defaultSuccessUrl,defaultSuccessUrl是只登陆成功后重定向的页面,使用failureHandler也是因为相同的缘由。
5.configure(WebSecurity web)方法中我配置了一些过滤规则,不赘述。
6.另外,对于静态文件,如/images/**/css/**/js/**这些路径,这里默认都是不拦截的。

Controller

最后来看看咱们的Controller,以下:

@RestController
public class LoginRegController {

    /**
     * 若是自动跳转到这个页面,说明用户未登陆,返回相应的提示便可
     * <p>
     * 若是要支持表单登陆,能够在这个方法中判断请求的类型,进而决定返回JSON仍是HTML页面
     *
     * @return
     */
    @RequestMapping("/login_page")
    public RespBean loginPage() {
        return new RespBean("error", "还没有登陆,请登陆!");
    }
}

这个Controller总体来讲仍是比较简单的,RespBean一个响应bean,返回一段简单的json,不赘述,这里须要小伙伴注意的是login_page,咱们配置的登陆页面是一个login_page,但实际上login_page并非一个页面,而是返回一段JSON,这是由于当我未登陆就去访问其余页面时Spring Security会自动跳转到到login_page页面,可是在Ajax请求中,不须要这种跳转,我要的只是是否登陆的提示,因此这里返回json便可。

测试

最后小伙伴可使用POSTMAN或者RESTClient等工具来测试登陆和权限问题,我就不演示了。

Ok,通过上文的介绍,想必小伙伴们对Spring Boot+Spring Security处理Ajax登陆请求已经有所了解了,好了,本文就说到这里,有问题欢迎留言讨论。

更多资料请关注公众号:

图片描述

相关文章
相关标签/搜索