前后台分离的情况下使用SpringSecurity实现登录以及鉴权

简介

本文主要记录自己的学习历程。

1. SpringSecurity简介

2. 前言

观看此文应当具备一定的前后台分离的知识和了解MVC分层开发的概念,以及了解JWT(JsonWebToken),和无状态登录的概念,因笔者水平原因,有说的不对的地方,希望有大佬能够斧正。

3. 准备工作

首先为了模拟前后台分离开发过程中前台对后台的请求,我们使用Postman进行模拟,后台工程使用springboot整合SpringSecurity以及JWT(JsonWebToken)等,对于数据库的操作使用MyBatis-Plus进行操作,到此准备工作基本完成。

4. 构建后台项目

构建一个SpringBoot项目
可参考依赖

	<dependency>
           <groupId>org.springframework.bootgroupId>
           <artifactId>spring-boot-starter-jdbcartifactId>
       dependency>
       <dependency>
           <groupId>org.springframework.bootgroupId>
           <artifactId>spring-boot-starter-securityartifactId>
       dependency>
       <dependency>
           <groupId>org.springframework.bootgroupId>
           <artifactId>spring-boot-starter-webartifactId>
       dependency>
       <dependency>
           <groupId>com.alibabagroupId>
           <artifactId>druid-spring-boot-starterartifactId>
           <version>1.2.8version>
       dependency>
       <dependency>
           <groupId>com.baomidougroupId>
           <artifactId>mybatis-plus-boot-starterartifactId>
           <version>3.4.3.4version>
       dependency>
       <dependency>
           <groupId>io.jsonwebtokengroupId>
           <artifactId>jjwtartifactId>
           <version>0.9.0version>
       dependency>
       <dependency>
           <groupId>javax.xml.bindgroupId>
           <artifactId>jaxb-apiartifactId>
           <version>2.3.1version>
       dependency>
       <dependency>
           <groupId>org.springframework.bootgroupId>
           <artifactId>spring-boot-devtoolsartifactId>
           <scope>runtimescope>
           <optional>trueoptional>
       dependency>
       <dependency>
           <groupId>mysqlgroupId>
           <artifactId>mysql-connector-javaartifactId>
           <scope>runtimescope>
       dependency>
       <dependency>
           <groupId>org.projectlombokgroupId>
           <artifactId>lombokartifactId>
           <optional>trueoptional>
       dependency>
       <dependency>
           <groupId>org.springframework.bootgroupId>
           <artifactId>spring-boot-starter-testartifactId>
           <scope>testscope>
       dependency>
       <dependency>
           <groupId>org.springframework.securitygroupId>
           <artifactId>spring-security-testartifactId>
           <scope>testscope>
       dependency>

更改配置文件application.yml

server:
  port: 8991
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      test-on-borrow: true
      test-while-idle: true
mybatis-plus:
  global-config:
    db-config:
      id-type: auto
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true

这里我设置后台工程的端口号为8991,其余配置为数据库的连接信息,以及设置使用druid的数据源和开启sql的日志以便于我们debug

创建数据库

此文仅仅作为一个demo演示,因此我们只需要创建一个User表即可
表字段:
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第1张图片
id 为主键 int 类型设置自增即可
username 用户名 可以设置此字段不可重复
password 用户密码字段 长度设置为300是因为 我们会把用户的密码加密存储到表中 所以字段长度大一些
role 用户的角色,对于用户的鉴权操作我们要基于此字段

以上就是数据库的所有信息。

编写实体类

首先编写User实体类

package com.mrr.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
    // 设置主键自增策略
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String username;
    private String password;
    private String role;
}

使用lombok插件可以快速帮我们生成getter,setter方法
@Accessors(chain = true) 此注解是用来开启链式编程的(没什么太大的用处,就是为了方便写代码,不了解也没关系)

开发dao/mapper层

使用mybatis-plus可以帮助我们快速构建dao层
创建UserMapper接口继承BaseMapper即可(BaseMapper需要传入User实体类的泛型)

package com.mrr.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mrr.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface UserMapper extends BaseMapper<User> {
}

mybatis-plus已经为我们实现了很多的操作,我们不需要再写抽象方法了

本文为了演示方便,就不在写service层了,直接在controller层调用mapper层。

构造一个返回信息模板类

为什么要构建

因为前后台分离过程中,他们之间需要通过JSON字符串传递信息。因此为了便于整理,我们写一个模板。

构建

根据我的个人习惯会创建一个包templates,在这个包下创建一个类BaseBackRes
其中的额代码:

package com.mrr.templates;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

@Data
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
public class BaseBackRes<T> {
    private Integer code;
    private String message;
    private T data;
}

code 为我们定义的返回状态码
message 为我们返回的信息
data 我们可以封装一些信息放在里面

到此返回模板的构造工作也已经完成。

构建主要内容

接下来是我们的主要部分
首先理清一下逻辑,首先前台的登陆页面肯定是一个表单,提交表单后会请求后台的登录接口,请求的参数就是表单封装的对象,使用SpringSecurity完成登录操作,我们要做的是,根据前台传过来的用户名
去数据库进行查找,如果能查找出字段就在判断密码是否正确,以及后续的操作(这些都是SpringSecurity来做)
因此需要我们构建一个UserDetailServiceImpl去实现UserDetaiService接口,这个接口是SpringSecurity封装的,里面只有一个方法
创建一个类UserDetailServiceImpl

package com.mrr.config;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mrr.mapper.UserMapper;
import com.mrr.pojo.DetailUser;
import com.mrr.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
	// 注入我们的mapper层
    @Autowired
    UserMapper userMapper;

	// 重写方法
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper wrapper = new QueryWrapper();
        wrapper.eq("username",username);
        User user = userMapper.selectOne(wrapper);
        return new DetailUser(user);
    }
}

注意我们返回的不是User对象,那是因为这个方法需要返回一个实现了UserDetails接口的类
因此我们构建一个DetailUser

package com.mrr.pojo;

import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

@Data
@Accessors(chain = true)
public class DetailUser implements UserDetails {
	// 对应我们User的id username password字段即可此处不需要对应role字段
    private Integer id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public DetailUser() {
    }

    // 写一个能直接使用user创建DetailUser的构造器
    public DetailUser(User user) {
        id = user.getId();
        username = user.getUsername();
        password = user.getPassword();
        authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
    }

    // 获取权限信息,我们存role字段(可以忽略)
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return username;
    }

    // 账号是否未过期,默认是false,记为true
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账号是否未锁定,默认是false,记为true
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 账号凭证是否未过期,默认是false,记为true
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //默认是false,记为true
    @Override
    public boolean isEnabled() {
        return true;
    }

    // toString
    @Override
    public String toString() {
        return "DetailUser{" +
                "id=" + id +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                ", authorities=" + authorities +
                '}';
    }
}

这个类我们用来做登录校验使用

构建一个JWT工具类

接下来是构建另外一个重点
此处代码,笔者参考了网上的教程,基本思想都差不多,大家复制即可

package com.mrr.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;

public class JwtTokenUtils {

    public static final String TOKEN_HEADER = "Authorization";
    public static final String TOKEN_PREFIX = "Bearer ";

    private static final String SECRET = "jwtsecretdemo";
    private static final String ISS = "echisan";

    // 过期时间是1个小时
    private static final long EXPIRATION = 3600L;

    // 选择了记住我之后的过期时间为7天
    private static final long EXPIRATION_REMEMBER = 604800L;

    // 添加角色前缀
    private static final String ROLE_CLAIMS = "rol";

    // 创建token
    public static String createToken(String username,String role, boolean isRememberMe) {
        long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
        HashMap<String, Object> map = new HashMap<>();
        map.put(ROLE_CLAIMS, role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .setClaims(map)
                .setIssuer(ISS)
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }

    // 从token中获取用户名
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }

    // 获取用户角色
    public static String getUserRole(String token){
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }

    // 是否已过期
    public static boolean isExpiration(String token) {
        try {
            return getTokenBody(token).getExpiration().before(new Date());
        } catch (ExpiredJwtException e) {
            return true;
        }
    }

    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

构建过滤器

过滤器我们需要构建两个,第一个是做认证的,此文中指的是登录认证,另一个是鉴权的
首先构建认证过滤器JWTAuthenticationFilter它需要实现UsernamePasswordAuthenticationFilter接口

package com.mrr.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.pojo.DetailUser;
import com.mrr.pojo.LoginUser;
import com.mrr.templates.BaseBackRes;
import com.mrr.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;

public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    // 记住我功能,本文暂未实现,但是先写出来
    private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
    
    // 固定代码
    private AuthenticationManager authenticationManager;
    
    // 一个构造方法
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // 这里指定登录请求的接口(我们可以把这个类当作登录功能的controller层)不指定默认为 /login
        super.setFilterProcessesUrl("/users/login");
    }
    
    // 核心方法
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        // 从输入流中获取到登录的信息
        try {
            LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
            rememberMe.set(loginUser.getRememberMe() == null ? 0 : loginUser.getRememberMe());
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        // 调用getPrincipal()方法会返回一个实现了UserDetails接口的对象,所以这里我们就返回上文定义的DetailUser
        response.setContentType("application/json;charset=utf-8");
        DetailUser detailUser = (DetailUser) authResult.getPrincipal();
        boolean isRemember = rememberMe.get() == 1;
        // 获取用户角色信息
        String role = "";
        Collection<? extends GrantedAuthority> authorities = detailUser.getAuthorities();
        for (GrantedAuthority authority : authorities){
            role = authority.getAuthority();
        }
        // 登陆成功,生成JWT并放在响应头里,并封装成JSON格式返回给前台
        String token = JwtTokenUtils.createToken(detailUser.getUsername(), role,isRemember);
        response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);

        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        // 我们前面定义的模板类,把密码置空,防止隐私信息暴露
        BaseBackRes res = new BaseBackRes().setCode(200).setMessage("登录成功").setData(((DetailUser) authResult.getPrincipal()).setPassword(null)); 
        writer.write(new ObjectMapper().writeValueAsString(res));
        writer.flush();
        writer.close();

    }

    // 这是验证失败时候调用的方法(登陆失败)
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        // 提示用户名或密码错误就行了
        BaseBackRes res = new BaseBackRes().setCode(408).setMessage("用户名或密码错误");
        writer.write(new ObjectMapper().writeValueAsString(res));
        writer.flush();
        writer.close();
    }
}

具体的含义请移步至代码的注释部分
LoginUser

package com.mrr.pojo;

public class LoginUser {

    private String username;
    private String password;
    private Integer rememberMe;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getRememberMe() {
        return rememberMe;
    }

    public void setRememberMe(Integer rememberMe) {
        this.rememberMe = rememberMe;
    }
}

到此为止,第一个认证的过滤器就已经实现好了。

下面我们来实现第二个授权过滤器 JWTAuthorizationFilter
实现这个的方式有很多,但是本文就是用这一种,大家实现的方式基本都是大差不差,读者直接复制就行

package com.mrr.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.exception.TokenIsExpiredException;
import com.mrr.templates.BaseBackRes;
import com.mrr.utils.JwtTokenUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;

public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接结束
        if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        try {
            SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        } catch (Exception e) {
            Throwable cause = e.getCause();
            //返回json形式的错误信息
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json; charset=utf-8");
            if(cause instanceof TokenIsExpiredException){
                PrintWriter writer = response.getWriter();
                BaseBackRes res = new BaseBackRes().setCode(410).setMessage("服务器错误,稍后再试");
                writer.write(new ObjectMapper().writeValueAsString(res));
                writer.flush();
                writer.close();
                return;
            }
            return;
        }
        super.doFilterInternal(request, response, chain);
    }

    // 这里从token中获取用户信息并新建一个token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException {
        String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
        boolean expiration = JwtTokenUtils.isExpiration(token);
        if (expiration) {
            throw new TokenIsExpiredException("token超时");
        } else {
            String username = JwtTokenUtils.getUsername(token);
            String role = JwtTokenUtils.getUserRole(token);
            if (username != null) {
                return new UsernamePasswordAuthenticationToken(username, null,
                        Collections.singleton(new SimpleGrantedAuthority(role))
                );
            }
        }
        return null;
    }
}

此处有些难懂,笔者的理解也不是很深刻,但是这样已经可以实现鉴权操作了,有兴趣的读者下去之后可以继续研究
TokenIsExpiredException类

package com.mrr.exception;

public class TokenIsExpiredException extends Exception {


    public TokenIsExpiredException() {
    }

    public TokenIsExpiredException(String message) {
        super(message);
    }

    public TokenIsExpiredException(String message, Throwable cause) {
        super(message, cause);
    }

    public TokenIsExpiredException(Throwable cause) {
        super(cause);
    }

    public TokenIsExpiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

好的这个时候我们已经写的差不多了,接下来是最后一个重点,也是核心配置类SecurityConfig

package com.mrr.config;

import com.mrr.filter.JWTAuthenticationFilter;
import com.mrr.filter.JWTAuthorizationFilter;
import com.mrr.handler.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    /**
     * @EnableWebSecurity 此注解必须加上
     * @Configuration 此注解必须加上
     * @EnableGlobalMethodSecurity(prePostEnabled = true) 配置这个注解后,可以方便我们管理什么样的角色才能访问接口
     */

    // UserDetailsService的实现类太多,我们要注入自己的
    @Autowired
    @Qualifier("userDetailsServiceImpl")
    private UserDetailsService userDetailsService;

    // 用户未登录处理器
    @Autowired
    private NoEntryPoint noEntryPoint;

    // 用户没有权限的处理器
    @Autowired
    private NoPermissionHandler noPermissionHandler;

    // 用户的密码加密方式,我们使用Spring推荐的这个就可以了
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // 设置密码加密器
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }

    // 配置权限的优先级别 此处指的是admin的权限大于user 即高权限角色应当自动拥有低权限角色的权限(需要加上ROLE_前缀)
    @Bean
    RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER");
        return hierarchy;
    }

    // 配置的核心方法
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                // 权限设置管理
                .authorizeRequests()
                // tests下的所有接口,只有有ROLE_ADMIN角色的用户才能访问
                .antMatchers(HttpMethod.DELETE, "/tests/**").hasAuthority("ROLE_ADMIN")
                // 此处代表放行的资源路径
                .antMatchers("/users/login", "/users/register").permitAll()
                // 所有请求都需要授权除了放行的(此处的授权指的是登录)
                .anyRequest().authenticated() // 这句话的意思是所有请求都需要登录,除了上面已经放行的
                .and()
                // 没有登录的处理器
                .httpBasic().authenticationEntryPoint(noEntryPoint)
                .and()
                // 权限不足的处理器
                .exceptionHandling().accessDeniedHandler(noPermissionHandler)
                .and()
                //开启表单登录
                .formLogin()
                .and()
                // 使用我们自己的过滤器覆盖默认的,要不然我们的不生效
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 禁用session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    
    // 一些允许跨域的操作
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}


详细含义,请移步至注释
到此为止,我们关于SpringSecurity的配置就差不多了

处理器的定义

新建一个包handler

用户未登录处理器 NoEntryPoint

package com.mrr.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.templates.BaseBackRes;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component
public class NoEntryPoint implements AuthenticationEntryPoint {
    /**
     * 没有登录的处理器
     * @param request
     * @param response
     * @param authException
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        BaseBackRes res = new BaseBackRes().setCode(409).setMessage("请先登录");
        writer.write(new ObjectMapper().writeValueAsString(res));
        writer.flush();
        writer.close();
    }
}

用户没有权限的处理器 NoPermissionHandler

package com.mrr.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.mrr.templates.BaseBackRes;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@Component
public class NoPermissionHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter writer = httpServletResponse.getWriter();
        BaseBackRes res = new BaseBackRes().setCode(409).setMessage("您没有此权限");
        writer.write(new ObjectMapper().writeValueAsString(res));
        writer.flush();
        writer.close();
    }
}

开发用户注册的控制器

package com.mrr.controller;

import com.mrr.mapper.UserMapper;
import com.mrr.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;

@RestController
@RequestMapping("/users")
public class UserController {
    // 注入mapper层 不在写service层了
    @Autowired
    private UserMapper userMapper;
    
    // 注入密码加密器
    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @PostMapping("/register")
    public HashMap<String, Object> registerUser(@RequestBody User registerUser){
        // 密码入数据库之前先加密
        registerUser.setPassword(bCryptPasswordEncoder.encode(registerUser.getPassword()));
        // 用户注册默认只有USER角色
        registerUser.setRole("ROLE_USER");
        // 入库
        int insert = userMapper.insert(registerUser);
        // 返回结果这里就用map模拟了,不在构建模板类了
        HashMap<String, Object> map = new HashMap<>();
        if(insert > 0){
            map.put("code",200);
            map.put("message","注册成功");
            return map;
        }
        map.put("code",406);
        map.put("message","注册失败");
        return map;
    }

}

具体含义,请移步至注释

测试鉴权的控制器

package com.mrr.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/tests")
public class TestController {

    @PreAuthorize("hasRole('ADMIN')") // 此注解可以快速指定什么样的请求路径需要什么样的角色,因为我们在SecurityConfig中开启了这个注解
    @GetMapping("/getList")
    public String getList(){
        return "得到一个列表";
    }

    @PostMapping("createList")
    public String createList(){
        return "创建一个列表";
    }

    @PutMapping("/updateList/{testId}")
    public String updateList(@PathVariable("testId")Integer id){
        return "更新id为:"+id+"的列表";
    }

    @DeleteMapping("/deleteList/{testId}")
    public String deleteList(@PathVariable("testId")Integer id){
        return "删除id为:"+id+"的列表";
    }
}

测试结果

使用PostMan进行测试

1. 注册一个用户

前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第2张图片前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第3张图片
注册成功,并且密码也已经加密了。

登录接口

前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第4张图片
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第5张图片
登录没有问题,并且可以在响应头中看到我们颁发的这个Token

密码或用户名输错的情况下访问登录接口

前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第6张图片

鉴权

这些请求需要先复制刚才的Token(注意不要复制前面的Bearer)
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第7张图片
请求我们的测试接口时放在请求头中
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第8张图片
admin2账户没有ROLE_ADMIN角色因此,提示没有权限
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第9张图片
createList接口不需要ROLE_ADMIN权限,因此请求成功。

手动修改admin2用户的角色为ROLE_ADMIN,在进行测试
前后台分离的情况下使用SpringSecurity实现登录以及鉴权_第10张图片
请求成功(这个测试需要再次重新登录)
剩下的接口不在进行测试,原理都是一样的。笔者在此就省略了。

结语

安全校验的内容主要分为两块,认证和授权。只要掌握这个方向,再去学习SpringSecurity就会更加具有目的性。
SpringSecurity的配置总体来说比较复杂和繁琐,但是为了确保我们开发出的系统拥有较高的安全性,再复杂的配置也是应该的。
以上就是本文的全部内容!

参考

文章一
文章二

你可能感兴趣的:(前后台分离,java,spring,boot,postman,maven,intellij-idea)