Spring Security

对于权限的管理,在企业应用程序的开发中,是必不可少的功能,但是能够灵活且强大的权限控制又不是一件容易的事情,所以在自己学习编写权限控制体系的基础上也接触一下成熟的框架,Spring 的全家桶系列 Spring Security 就进入了我们的视线。

Spring Security,这是一种基于 Spring AOP 和 Servlet 过滤器的安全框架。它提供全面的安全性解决方案,同时在 Web 请求级和方法调用级处理身份确认和授权。

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是那个用户。
  • 授权:经过认证后判断当前用户是否有权限进行某个操作。

而认证和授权也是 Spring Security 作为安全框架的核心功能。

入门案例

我们通过一个简单的入门案例来了解 Spring Security 。

引入依赖

我们已父子项目方式搭建,所有的依赖都在父项目中书写,子项目只需继承父项目即可。父项目中的写法:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.0</version>
</parent>
<dependencies>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>        
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
    </dependency>
    <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
</dependencies>

控制类

书写一个普通的 controller 类

@RestController
@RequestMapping("/guanwei")
public class GuanweiController {

    @GetMapping
    public String execute() {
        return "This is GuanweiController`execute!";
    }
}

启动类 

@SpringBootApplication
public class Security01Application {
    public static void main(String[] args) {
        SpringApplication.run(Security01Application.class, args);
    }
}

运行效果

当我们还按照过去的方式访问 http://localhost:8081/guanwei 路径时,发现弹出了登录框,这个是 Spring Security 自带的登录页面,要求我们必须登录后才能访问系统资源。 

并且在项目启动后,在控制台能看到初始生成的密码。

使用 Spring Security 后访问系统任意资源时,就会跳转到默认登录页面,默认账号是 user ,登录成功后,才能访问才能对目标接口进行访问。以后只要不关闭浏览器或者服务器,都可以直接访问。也可以手动登出,路径是:logout 。 

登陆认证

我们刚才通过一个简单的案例,了解了 Spring Security 的基本概念。也发现 Spring Security 会在服务器启动时随机生成密码,那么有的童鞋就会想到,能不能自己去定义这个密码,甚至于使用数据库来校验用户登陆。

自定义账号密码

这种方式其实很简单,只需要在配置文件 (application.yml) 中设置账号密码就行。

spring:
  security:
    user:
      name: root
      password: guanwei

还可以在配置类中进行设置,注释上边的写法,在 config 包下创建一个配置类 SecurityConfig 来配置账号密码信息。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String pass = passwordEncoder.encode("123456");
        auth.inMemoryAuthentication().withUser("guanwei").password(pass).roles("admin");
    }
    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

BCryptPasswordEncoder 类是 Spring Security 中的一个加密方法类。BCryptPasswordEncoder 方法采用了 SHA-256+随机盐+密钥对密码进行加密。 SHA 是一种安全 Hash 函数(SHA),是使用最广泛的Hash函数。
加密算法与 hash 算法的区别:

  • 加密算法是可逆的,加密算法的基本过程是对原来为明文的数据按某种算法进行处理,使其成为不可读的一段代码为“密文”,但在用相应的密钥进行操作之后就可以得到原来的内容 。
  • hash 算法是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。同时,哈希函数可以将任意长度的输入经过变化以后得到固定长度的输出。

数据库校验方式

前面两种都是写死的,可能对于固定的超级管理员可以用,我们真实的项目场景肯定都是数据库里面的,肯定需要自定义查询,我们先把SecurityConfig 注释了,重新创建一个类 SpringSecurityConfig 来从数据库中判断账号密码。

引入数据库相关依赖

<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <scope>runtime</scope>
</dependency>
<dependency>
     <groupId>com.baomidou</groupId>
     <artifactId>mybatis-plus-boot-starter</artifactId>
     <version>3.5.1</version>
</dependency>
<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid-spring-boot-starter</artifactId>
     <version>1.2.9</version>
</dependency>

书写 bean 包的类

@Data
public class Users implements Serializable {
    @TableId(value = "id",type = IdType.AUTO)
    private Integer id;
    private String userName;
    private String passWord;
    private String nickName;
}

书写 mapper 包的类

@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}

书写一个 service 类继承 UserDetailsService 

@Service
public class LoginUsersServiceImpl implements UserDetailsService {
    @Resource
    private UsersMapper mapper;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<Users> query = new QueryWrapper<>();
        query.eq("username", username);
        Users users = mapper.selectOne(query);
        // 如果没有找到用户名 抛出异常 失败
        if (users == null) {
            System.out.println("账号或者密码错误");
            throw new UsernameNotFoundException("账号或者密码错误");
        }
        // 这里是加载用户权限,这里先模拟个 admin 权限,更详细的在后边会说到
        List<GrantedAuthority> auths = AuthorityUtils.createAuthorityList("admin");
        return new User(username, new BCryptPasswordEncoder().encode(users.getPassWord()), auths);
    }
}

书写 SpringSecurityConfig 配置类

@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(getPasswordEncoder());
    }
}

这时候继续访问刚才的页面,输入数据库的账号和密码就行了。

自定义登录页面

我们刚才一直使用 security 自带的登陆页面,但是在实际使用中我们更多的是使用我们自己书写的登陆页面,要想设置其实很简单,如下几步就行。

创建登录页面

<!--
  表单提交用户信息,注意
  1.账号和密码的名字
  2.action的提交地址和配置类中设置一致
-->
<form action="/user/login" method="post">
    账号:<input type="text" name="username"/><br/>
    密码:<input type="password" name="password"/><br/>
    <button>登录</button>
</form>

在配置类中设置登录页面 

    // 自定义登录页面
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义登陆页面
                .loginPage("/login.html") //登录页面设置
                .loginProcessingUrl("/user/login")     //登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()   //登陆成功之后,跳转路径
                .and().authorizeRequests()
                .antMatchers( "/login.html", "/user/login").permitAll()  //设置哪些路径可以直接访问,不需要认证
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

授权管理

在项目中,有很多接口是针对不同角色权限的,如果角色是超级管理员,就拥有访问所有权限的能力,如果不是超级管理员,访问其他接口是不能允许的。 有些用户具有部分权限,就可以访问这些权限所能访问的内容,如果要实现这种效果,我们就需要授权管理。

基于权限

hasAuthority()方法:如果当前的主体具有指定的权限,则返回 true,否则返回 false。

配置类

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()   //自定义登陆页面
                .loginPage("/login.html") //登录页面设置
                .loginProcessingUrl("/user/login")     //登陆访问路径  这里同 action
                .defaultSuccessUrl("/index.html").permitAll()   //登陆成功之后,跳转路径
                .and().authorizeRequests()
                .antMatchers( "/login.html", "/user/login").permitAll()  //设置哪些路径可以直接访问,不需要认证
                .antMatchers("/index.html").hasAuthority("guanwei") // 设置当前登录用户访问index.html页面时需要guanwei权限
                // 设置当前登录用户访问1.html页面时需要manager1、manager2、manager3或manager4权限
                .antMatchers("/1.html").hasAnyAuthority("manager1","manager2","manager3","manager4")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf
    }

  

这里我们可以看到访问 index.html 页面需要 guanwei 权限,而访问1.html 只要有 manager1、manager2、manager3 或 manager4 权限之一就行。

业务类

还是使用刚才的权限 guanwei,然后看看运行结果。

运行结果

访问 index.html 页面

访问 1.html 页面

我们更换权限为 manager1 来试一试

这里就可以直接访问了。 

基于角色

配置类

业务类

运行结果

配置 403 页面

当没有权限的时候访问会报403,我们可以自定义一个页面。

创建一个 403.html 页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>403</title>
</head>
<body>
<h2>你没有相应的权限访问这个页面!</h2>
</body>
</html>

配置类中设置

运行结果

  

用户注销

SpringSecurity 的注销功能很简单,只需要一个超链接地址为 /logout 就行。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
我是注销页面!
<a href="/logout">注销</a>
</body>
</html>

分离项目

通过刚才的案例我们了解到了 SpringSecurity 认证授权的效果。可是刚才通过的是表单方式进行提交,在其后的项目中我们可能更多的使用前后端分离效果,那么我们就要返回给前端对应的消息来告诉前端认证授权的情况。

前期准备

前后端分离项目,需要后端返回消息来标注相应状态,这时候我们创建三个类来对返回的消息统一格式。

统一返回格式

/**
 * @Author: Dailyblue
 * @Description: 统一返回实体
 * @Date Create in 2022/06/21 19:28
 */
@Data
public class JsonResult<T> implements Serializable {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;
    public JsonResult() {
    }
    public JsonResult(boolean success) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }

    public JsonResult(boolean success, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }
}

返回状态码定义


/**
 * @Author: Dailyblue
 * @Description: 返回码定义
 * 规定:
 * #1表示成功
 * #1001~1999 区间表示参数错误
 * #2001~2999 区间表示用户错误
 * #3001~3999 区间表示接口异常
 * @Date Create in 2022/06/21 19:28
 */
public enum ResultCode {
    /* 成功 */
    SUCCESS(200, "成功"),

    /* 默认失败 */
    COMMON_FAIL(999, "失败"),

    /* 参数错误:1000~1999 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    /* 业务错误 */
    NO_PERMISSION(3001, "没有权限");
    private Integer code;
    private String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根据code获取message
     *
     * @param code
     * @return
     */
    public static String getMessageByCode(Integer code) {
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}

返回体构造工具

/**
 * @Author: Dailyblue
 * @Description: 返回体构造工具
 * @Date Create in 2022/06/21 19:28
 */
public class ResultTool {
    public static JsonResult success() {
        return new JsonResult(true);
    }

    public static <T> JsonResult<T> success(T data) {
        return new JsonResult(true, data);
    }

    public static JsonResult fail() {
        return new JsonResult(false);
    }

    public static JsonResult fail(ResultCode resultEnum) {
        return new JsonResult(false, resultEnum);
    }
}

未登录效果

当用户未登录时,会自动进入当前类的 commence 方法,我们在这个方法中返回 JSON 格式的错误信息。

@Component
public class NotLoginAuthentication implements AuthenticationEntryPoint {
    // 当用户未登录时 访问资源会返回结果
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        JsonResult result = ResultTool.fail(ResultCode.USER_NOT_LOGIN);
        response.setContentType("text/json;charset=utf-8");
        response.getWriter().write(JSON.toJSONString(result));
    }
}

登录失败效果

当用户登录失败时(不论是账号未找到,密码错误还是权限问题),都会进入这个类的指定方式,我们在这个方法中返回 JSON 格式的错误信息。

@Component
public class FailureAuthenticationHandler implements AuthenticationFailureHandler {

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) 
                                    throws IOException, ServletException {
        //返回json数据
        JsonResult result;
        if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        } else {
            //其他错误
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSON.toJSONString(result));
    }
}

登录成功效果

同样的道理,对正确的消息返回 JSON 格式信息。

@Component
public class SuccessAuthenticationHandler implements AuthenticationSuccessHandler {

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        //返回json数据
        JsonResult result = ResultTool.success();
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSON.toJSONString(result));
    }
}

配置文件

在配置文件中注册三个效果。

protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .permitAll()
                .successHandler(successHandler)  //登录成功处理逻辑
                .failureHandler(failureHandler) //登录失败处理逻辑
                //异常处理(权限拒绝、登录失效等)
                .and()
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)//匿名用户访问无权限资源时的异常处理
                .and()
                .authorizeRequests()
                .antMatchers("/login.html", "/login").permitAll()
                .antMatchers("/dailyblue/guanwei").hasAnyAuthority("admin")
                .anyRequest().authenticated()   //所有请求都可以访问
                .and().csrf().disable();  //关闭csrf

    }

APIPost测试

登录成功的情况

登录失败的情况

没有登录就访问其他资源情况

进一步拓展

刚才的例子中,我们可以通过几个 Handler 操作未登录时、登录失败和登录成功情况。那么前后端分离情况下,如何保存用户登录状态呢?下边我们通过 Security 的几个过滤器来实现这个功能。

前期准备

我们这里通过 JWT 令牌方式来验证用户登录。JWT 登录详情可以查看另一篇博客。这里创建 JwtConfig 类。

package com.dailyblue.config;

import com.dailyblue.bean.Users;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @author dailyblue
 * @since 2022/6/23
 */
@Component
public class JwtConfig {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
    public static final String APP_SECRET = "1234"; //秘钥,加盐

    //	@param id 当前用户ID
    //	@param issuer 该JWT的签发者,是否使用是可选的
    //	@param subject 该JWT所面向的用户,是否使用是可选的
    //	@param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
    //	@param audience 接收该JWT的一方,是否使用是可选的
    // 生成json token字符串的方法
    public static String getJwtToken(Users user) {

        String jwtToken = Jwts.builder()
                .setHeaderParam("typ", "JWT")    //头部信息
                .setHeaderParam("alg", "HS256")    //头部信息
                //下面这部分是payload部分
                // 设置默认标签
                .setSubject("dailyblue")    //设置jwt所面向的用户
                .setIssuedAt(new Date())    //设置签证生效的时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))    //设置签证失效的时间
                //自定义的信息,这里存储id和姓名信息
                .claim("id", user.getId())  //设置token主体部分 ,存储用户信息
                .claim("name", user.getUserName())
                .claim("nickName", user.getNickName())
                //下面是第三部分
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
        // 生成的字符串就是jwt信息,这个通常要返回出去
        return jwtToken;
    }

    /**
     * 判断token是否存在与有效
     * 直接判断字符串形式的jwt字符串
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * 因为通常jwt都是在请求头中携带,此方法传入的参数是请求
     *
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");//注意名字必须为token才能获取到jwt
            if (StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token字符串获取会员id
     * 这个方法也直接从http的请求中获取id的
     *
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if (StringUtils.isEmpty(jwtToken)) return "";
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String) claims.get("id");
    }

    /**
     * 解析JWT
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
        return claims;
    }
}

SecurityUser 类,这个类描述用户信息和它的权限信息。

package com.dailyblue.bean;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * UserDetailService 使用该类,该类必须是 UserDetails 的子类
 */
@Data
public class SecurityUser implements UserDetails {

    // 登录用户的基本信息
    private Users user;

    //当前权限
    private List<String> permissionValueList;

    public SecurityUser() {
    }

    public SecurityUser(Users user) {
        if (user != null) {
            this.user = user;
        }
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        permissionValueList.forEach(permission ->{
            if(!StringUtils.isEmpty(permission)){
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
                authorities.add(authority);
            }
        });
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

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

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

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

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

UsersService 类,业务类,负责调用 Mapper 的方法

package com.dailyblue.service;

import com.dailyblue.bean.SecurityUser;
import com.dailyblue.bean.Users;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;

@Service
@Slf4j
public class UsersService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        log.info("进入了UsersService的loadUserByUsername方法,接受传递参数:{}", username);
        Users user = null;
		// 这里没有连接数据库 模拟数据
        if ("guanwei".equals(username)) {
            BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
            user = new Users(1, "guanwei", passwordEncoder.encode("123456"), "关为");
        }
        // 判断用户是否存在
        if (user == null) {
            throw new UsernameNotFoundException("账户信息不存在!");
        }
		// 这里没有连接数据库 模拟数据
        List<String> admin = Arrays.asList("manager1", "manager2");
        SecurityUser securityUser = new SecurityUser(user);
        securityUser.setPermissionValueList(admin);
        return securityUser;
    }
}

登录过滤器

这个是一个 Filter ,不需要 Spring 来注入

  • attemptAuthentication 方法 用户登录时触发,获取账号和密码,传递到我们自己书写的 UsersService 中。
  • successfulAuthentication 方法 登录成功后执行的方法,一般存放 token 到 Redis 中,返回成功信息。
  • unsuccessfulAuthentication 方法 登录失败后执行的方法。

后两个如果书写了,上一个案例中的那两个 Handler 就可以不书写了。

package com.dailyblue.filter;

import com.alibaba.fastjson.JSONArray;
import com.dailyblue.bean.SecurityUser;
import com.dailyblue.bean.Users;
import com.dailyblue.config.JwtConfig;
import com.dailyblue.util.JsonResult;
import com.dailyblue.util.ResultCode;
import com.dailyblue.util.ResultTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

@Slf4j
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    private JwtConfig jwtConfig;
    private AuthenticationManager authenticationManager;

    public TokenLoginFilter(JwtConfig jwtConfig, AuthenticationManager authenticationManager) {
        this.jwtConfig = jwtConfig;
        this.authenticationManager = authenticationManager;
        // 关闭登录只允许 post
        this.setPostOnly(false);
        // 设置登陆路径,并且post请求
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", "POST"));
    }

    // 获取登录页面传递过来的账号和密码
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("用户开始登录...");
        String userName = request.getParameter("userName");
        String password = request.getParameter("password");
        log.info("账号:{},密码:{}", userName, password);
        // 登录接口 /user/login 调用请求时触发
        // UsernamePasswordAuthenticationToken 封装登录时传递来的数据信息
        // 交给 AuthenticationManager  进行登录认证校验
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userName, password));
    }

    // 配置成功登录
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("用户登录成功!");
        // 认证成功之后,获取认证后的用户基本信息
        SecurityUser securityUser = (SecurityUser) authResult.getPrincipal();
        log.info("用户信息是:{}", securityUser);
        Users user = securityUser.getUser();
        String token = JwtConfig.getJwtToken(user);
        log.info("用户token是:{}", token);
        // token信息存于redis、数据库、缓存等
        //返回json数据
        JsonResult result = ResultTool.success();
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSONArray.toJSONString(result));
    }

    // 配置失败登录
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        log.info("用户登录失败!");
        //返回json数据
        JsonResult result;
        if (e instanceof AccountExpiredException) {
            //账号过期
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_EXPIRED);
        } else if (e instanceof BadCredentialsException) {
            //密码错误
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_ERROR);
        } else if (e instanceof CredentialsExpiredException) {
            //密码过期
            result = ResultTool.fail(ResultCode.USER_CREDENTIALS_EXPIRED);
        } else if (e instanceof DisabledException) {
            //账号不可用
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_DISABLE);
        } else if (e instanceof LockedException) {
            //账号锁定
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_LOCKED);
        } else if (e instanceof InternalAuthenticationServiceException) {
            //用户不存在
            result = ResultTool.fail(ResultCode.USER_ACCOUNT_NOT_EXIST);
        } else {
            //其他错误
            result = ResultTool.fail(ResultCode.COMMON_FAIL);
        }
        //处理编码方式,防止中文乱码的情况
        response.setContentType("text/json;charset=utf-8");
        //塞到response中返回给前台
        response.getWriter().write(JSONArray.toJSONString(result));
    }
}

验证过滤器

这个过滤器会在每次请求(不需要触发的可以在配置文件中设置)时去触发,主要作用是验证用户是否登录。

package com.dailyblue.filter;

import com.dailyblue.config.JwtConfig;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

@Slf4j
public class TokenAuthFilter extends BasicAuthenticationFilter {

    private JwtConfig jwtConfig;

    public TokenAuthFilter(AuthenticationManager authenticationManager, JwtConfig jwtConfig) {
        super(authenticationManager);
        this.jwtConfig = jwtConfig;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("开始校验用户是否登录");
        String token = request.getHeader("token");
        log.info("token:{}", token);
        if (token != null) {
            // 本次模拟 这里没有校验Redis
            Claims claims = JwtConfig.parseJWT(token);
            String nickName = claims.get("nickName").toString();
            log.info("获取的昵称是:{}", nickName);
            // 本次模拟 没有连接数据库
            List<String> permissionValueList = Arrays.asList("manager1", "manager2");
            Collection<GrantedAuthority> authority = new ArrayList<>();
            for (String permissionValue : permissionValueList) {
                SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);
                authority.add(auth);
            }
            UsernamePasswordAuthenticationToken upat = new UsernamePasswordAuthenticationToken(nickName, token, authority);
            // 有权限,则放入权限上下文中
            SecurityContextHolder.getContext().setAuthentication(upat);
        }
        chain.doFilter(request, response);
    }
}

配置文件

package com.dailyblue.config;

import com.dailyblue.filter.TokenAuthFilter;
import com.dailyblue.filter.TokenLoginFilter;
import com.dailyblue.handler.FailureAccessDeniedHandler;
import com.dailyblue.handler.NotLoginAuthentication;
import com.dailyblue.service.UsersService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)  // 方法增加权限
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtConfig jwtConfig;
    @Resource
    private UsersService usersService;
    @Resource
    private NotLoginAuthentication notLoginAuthentication;
    @Resource
    private FailureAccessDeniedHandler failureAccessDeniedHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        log.info("进入SecurityConfig的configure方法");
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        auth.userDetailsService(usersService).passwordEncoder(passwordEncoder);
    }

    /**
     * 配置访问过滤
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()
                .authenticationEntryPoint(notLoginAuthentication) // 未登录 handler
                .accessDeniedHandler(failureAccessDeniedHandler) // 无权限
                .and().csrf().disable() // 关闭 csrf 跨域请求
                .cors().and()    // security允许跨域
                .formLogin()
                .loginProcessingUrl("/user/login")  // 设定登录请求接口
                .usernameParameter("userName")
                .passwordParameter("password")
                .permitAll()
                .and()
                .authorizeRequests() // 请求设置
                .antMatchers("/guanwei").permitAll() // 配置不需要认证的接口
                .anyRequest().authenticated() // 任何请求都需要认证
                .and()
                .addFilter(new TokenLoginFilter(jwtConfig, authenticationManager())) // 认证交给 自定义 TokenLoginFilter 实现
                .addFilter(new TokenAuthFilter(authenticationManager(), jwtConfig))
                .httpBasic();
    }

    @Bean
    PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

加载流程

方法级别的权限控制

添加方法级别的角色控制,可以通过注解的方式来完成,在需要具有角色或权限方法的上引入 @PreAuthorize 注解。

这里为了方便期间,没有引入数据库,账号和密码都是写死的。

开启权限控制

控制层的方法上引入注解

@RestController
@RequestMapping("/guan")
public class GuanController {
    @GetMapping("/a")
    @PreAuthorize("hasAnyAuthority('admin')")
    public String a() {
        return "Hello,world!";
    }
    @GetMapping("/b")
    @PreAuthorize("hasAnyAuthority('admin','guan')")
    public String b() {
        return "This is method`b!";
    }
    @GetMapping("/c")
    @PreAuthorize("hasAnyAuthority('team')")
    public String c() {
        return "This is method`c!";
    }
}

效果演示

访问 a 方法

访问 b 方法 

SpringSecurity 基本流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,这里我们可以看看入门案例中的过滤器。

SpringSecurity 拦截器

1 . org.springframework.security.web.context.SecurityContextPersistenceFilter
     首当其冲的一个过滤器,作用之重要,自不必多言。
     SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个
     SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续filter建立所需的上下文。
     SecurityContext中存储了当前用户的认证以及权限信息。
2 . org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
     此过滤器用于集成SecurityContext到Spring异步执行机制中的WebAsyncManager
3 . org.springframework.security.web.header.HeaderWriterFilter
    向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
4 . org.springframework.security.web.csrf.CsrfFilter
    csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,
     如果不包含,则报错。起到防止csrf攻击的效果。
5. org.springframework.security.web.authentication.logout.LogoutFilter
    匹配 URL为/logout的请求,实现用户退出,清除认证信息。
6 . org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
    认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求。
7 . org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter
    如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。
8 . org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter
     由此过滤器可以生产一个默认的退出登录页面
9 . org.springframework.security.web.authentication.www.BasicAuthenticationFilter
    此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息。
10 . org.springframework.security.web.savedrequest.RequestCacheAwareFilter
    通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
11 . org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
   针对ServletRequest进行了一次包装,使得request具有更加丰富的API
12 . org.springframework.security.web.authentication.AnonymousAuthenticationFilter
   当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。
   spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
13 . org.springframework.security.web.session.SessionManagementFilter
   SecurityContextRepository限制同一用户开启多个会话的数量
14 . org.springframework.security.web.access.ExceptionTranslationFilter
   异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
15 . org.springframework.security.web.access.intercept.FilterSecurityInterceptor
   获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限。

WebSecurityConfigurerAdapter 接口

/**
 *  spring security 核心配置文件
 */
@Configuration
public class BrowerSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired  //自定义的安全元   数据源     实现FilterInvocationSecurityMetadataSource
    private MyInvocationSecurityMetadataSourceService myInvocationSecurityMetadataSourceService;
    @Autowired //自定义访问决策器
    private MyAccessDecisionManager myAccessDecisionManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         *  from表单登录设置
         */
        http.formLogin()
                .loginPage("")                      //登录页面                      /login
                .passwordParameter("")              //设置form表单中对应的name参数  默认为 password   下同
                .usernameParameter("")              //
                .defaultSuccessUrl("")            //认证成功后的跳转页面 默认跳转页面   可以设置是否总是默认  不是的话可以跳转与用户的target-url
                .failureUrl("")
                .failureForwardUrl("")            //登录失败 转发 的url
                .successForwardUrl("")              //登录成功 转发 的url  与successHandler对应  即处理完后请求转发的url
                .failureHandler(null)               //自定义的认证失败 做什么处理
                .successHandler(null)               //自定义认证成功 后做的处理    ----- 例如 想记录用户信息判断用户状态等
                .permitAll()                      //对于需要所有用户都可以访问的界面 或者url进行设置
                .loginProcessingUrl("")             //自定义处理认证的url    默认为    /login
                .authenticationDetailsSource(null)  //自定义身份验证的数据源  理解为查出数据库中的密码 和权限(可以不加) 然后再交给security
                修改和替换配置     已经配置好的修改   例如下面修改  安全拦截器的安全数据源
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    public <O extends FilterSecurityInterceptor> O postProcess(
                            O fsi) {
                        fsi.setPublishAuthorizationSuccess(true);
                        //修改成自定义的     安全元数据源  权限的源  !!!!!
                        fsi.setSecurityMetadataSource(myInvocationSecurityMetadataSourceService);
                        //修改成自定义的     访问决策器  自定义的
                        fsi.setAccessDecisionManager(myAccessDecisionManager);
                        //使用系统的
                        fsi.setAuthenticationManager(authenticationManager);
                        return fsi;
                    }
                });
        /**
         *  请求认证管理
         */
        http.authorizeRequests()
                .antMatchers("url匹配路径").permitAll()          //url匹配路径 permitAll 运行 全部访问 不用认证
                .accessDecisionManager(null)                                 //访问决策器
                .filterSecurityInterceptorOncePerRequest(true)               //过滤每个请求一次的安全拦截器 ???
                .anyRequest().authenticated()                                //其他的请求 需要认证,
                .antMatchers("/admin/**").hasRole("ADMIN")      //url匹配路径  具有怎样的角色
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")   //url匹配路径    具有怎样的角色 或者是权限
        ;
        /**
         *  anonymous
         *
         *  匿名访问时  存在默认 用户名  annonymousUser
         */
        http.anonymous().disable().csrf().disable();                         //禁止匿名  关闭csrf
        /**
         * 登出操作管理
         */
        http.logout()                                                        //登出处理
                .logoutUrl("/my/logout")
                .logoutSuccessUrl("/my/index")
                .logoutSuccessHandler(null)
                .invalidateHttpSession(true)
                .addLogoutHandler(null)
                .deleteCookies("cookieNamesToClear")
        ;
        /**
         *  session  会话管理
         */
        http.sessionManagement()                                            //session管理
                .maximumSessions(2)                                         //最大session 数量 --用户
                .maxSessionsPreventsLogin(false)                            //超过最大sessin数量后时候阻止登录
                .expiredUrl("/")                                            //会话失效后跳转的url
                .expiredSessionStrategy(null)                               //自定义session 过期错略
                .sessionRegistry(null)                                     //自定义的session 注册 表
        ;

    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /**
         *   基础的配置
         */
        auth
                /**
                 * 认证 时触发的事件
                 */
                .authenticationEventPublisher(null)
                /**
                 *  用户细节服务
                 *
                 *  认证管理器数据的来源 吧  用户身份凭证信息和 权限信息
                 */
                .userDetailsService(null)
                /**
                 *  密码编辑器 对密码进行加密
                 */
                .passwordEncoder(null)
        ;
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        /**
         * 不进行拦截的mvc
         */
        web.ignoring().mvcMatchers();
        /**
         * 添加自定义的 安全过滤器
         */
        web.addSecurityFilterChainBuilder(null);
    }
}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值