SpringSecurity(一)

个人认为,在框架中,最难的就是Spring与鉴权框架。大部分框架,即便不知道原理,知道如何使用,也能完成日常的开发。而鉴权框架和Spring不同,他们并没有限定如何去使用,更多的,需要程序员自己的想法。

我的文章不会写得很细,只会帮你完成一个可以运行的HelloWorld。

Maven依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.seaboot</groupId>
    <artifactId>security</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.SR5</spring-cloud.version>
    </properties>

    <dependencies>
        <!--<dependency>-->
            <!--<groupId>org.springframework.cloud</groupId>-->
            <!--<artifactId>spring-cloud-starter-oauth2</artifactId>-->
        <!--</dependency>-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

Controller

主要测试如何获取当前登录用户的信息

package cn.seaboot.security.ctrl;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author Mr.css
 * @date 2020-05-06 15:06
 */
@RestController
public class HelloController {

  @GetMapping("/hello")
  public String hello() {
    //获取登录的账号
    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    System.out.println(principal.getClass());
    System.out.println(principal);
    return "hello";
  }
}

SecurityConfig 

主要配置都包含在此接口中,按照自己的实际需求调整即可。

package cn.seaboot.security.config;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Mr.css
 * @date 2020-05-07 23:38
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  /**
   * 下面这两行配置表示在内存中配置了两个用户,分别是javaboy和lisi,密码都是123,并且赋予了admin权限
   * @param auth AuthenticationManagerBuilder
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("javaboy")
        .roles("admin")
        .password("$2a$10$Wuts2iHTzQBmeRVKJ21oFuTsvOJ5ffsqpD3DRzNupKwn5Gy54LEpC")
        .and()
        .withUser("lisi")
        .roles("user")
        .password("$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS");
  }

  /**
   * 加密算法
   */
  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  /**
   * URL角色权限配置,下列代码的意思是:访问路径hello,需要有admin角色
   *
   * @param http HttpSecurity
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
        .antMatchers("/hello/**").hasRole("admin")
        .antMatchers("/hello").hasRole("admin")
        .anyRequest().authenticated()
        .and()
        .formLogin().and()
        .httpBasic();
  }

  /**
   * 白名单配置:直接过滤掉该地址,即该地址不走 Spring Security 过滤器链
   */
  @Override
  public void configure(WebSecurity web){
    web.ignoring().antMatchers("/vercode");
  }

  /**
   * 测试加密算法
   * @param args
   */
  public static void main(String[] args) {
    System.out.println(new BCryptPasswordEncoder().encode("123"));;
    System.out.println(new BCryptPasswordEncoder().matches("123", "$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS"));;
  }
}

URL权限配置的其它可选项:

在configure函数中,已经展示了如何给Url配置权限,更多的配置如下:

antMatchers(url).hasRole()
antMatchers(url).access()

hasRole([role]) 当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
hasAuthority([auth]) 等同于hasRole
hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
Principle 代表当前用户的principle对象
authentication 直接从SecurityContext获取的当前Authentication对象
permitAll 总是返回true,表示允许所有的
denyAll 总是返回false,表示拒绝所有的
isAnonymous() 当前用户是否是一个匿名用户
isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
isAuthenticated() 表示当前用户是否已经登录认证成功了。
isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。

测试

访问Hello地址,就会自动跳转登录页面(Spring Security内置),用自己配置的账号即可登录。

问题一

思考问题:我们的用户肯定是配置在数据库里的,登录页面也必定是用自己的,上述代码肯定不满足我们日常使用,我们该怎么编写我们需要的代码?

答:通过formLogin()可以配置我们自己的登录页面,表单提交路径,以及首页地址。

配置如下:

 http
        .formLogin()
        .loginPage("/login.html")
        .failureUrl("/login.html?error=1")
        .defaultSuccessUrl("/index.html")
        .loginProcessingUrl("/user/login")
        .permitAll()
        .and()

登录的接口如下:

package org.springframework.security.core.userdetails;

public interface UserDetailsService {
  UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}

问题二:

思考问题:上述的接口,只有1个参数 UserName,那么问题就来了,假设我们有2个参数怎么办?比如:验证码。

答:可以添加一个登录前置拦截,先验证验证码的有效性,然后再走我们的正常流程。

http.addFilterBefore(new xxxxxxFilter(), xxxxxxxFilter.class)

 

SecurityConfig进阶

根据上述问题,对代码进行调整

package cn.seaboot.security.config;

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.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.annotation.Resource;
import javax.sql.DataSource;

/**
 * @author Mr.css
 * @date 2020-05-07 23:38
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
  /**
   * @param auth AuthenticationManagerBuilder
   */
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 设置自定义的userDetailsService
    auth.userDetailsService(new CustomUserDetailsService())
        .passwordEncoder(passwordEncoder());
  }

  /**
   * 加密算法
   */
  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  /**
   * URL角色权限配置,访问路径hello,需要有admin角色
   *
   * @param http HttpSecurity
   */
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
        .addFilterBefore(new BeforeLoginAuthenticationFilter("/user/login", "/login.html"), UsernamePasswordAuthenticationFilter.class)
        .authorizeRequests()
        .antMatchers("/hello/**").hasRole("admin")
        .antMatchers("/hello").hasRole("admin")
        .anyRequest().authenticated()
        .and()
        .formLogin()
        .loginPage("/login.html")
        .failureUrl("/login.html?error=1")
        .defaultSuccessUrl("/index.html")
        .loginProcessingUrl("/user/login")
        .permitAll()
        .and()
        .httpBasic();


    //session管理
    //session失效后跳转到登录页面
    http.sessionManagement().invalidSessionUrl("/toLogin");

    //单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
    //http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy());

    //单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
    http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);

    //默认的登录页面有一个用于安全验证的token,如果使用模版引擎,可以使用表达式获取,这里直接使用html,因此先禁用
    //    <input name="_csrf" type="hidden" value="d2ef6916-316b-4889-895c-07a2ca3759fc">
    //    <input  type = “hidden”  name = “${_csrf.parameterName}”  value = “${_csrf.token}” />
    http.csrf().disable();
  }

  /**
   * 白名单配置:直接过滤掉该地址,即该地址不走 Spring Security 过滤器链
   */
  @Override
  public void configure(WebSecurity web) {
    web.ignoring().antMatchers("/vercode");
  }
}

模拟用户登录

package cn.seaboot.security.config;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

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

/**
 *
 * @author Mr.css
 * @date 2020-05-08 0:02
 */
public class CustomUserDetailsService implements UserDetailsService {

  @Override
  public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    // TODO: 查询账户
    // 这里并没有真正去查询数据库,而是允许任意账号登录,密码都是123,并且都是admin角色
    // GrantedAuthority直译是授予权限,与config中配置的hasRole有歧义,但是功能上其实是一样的
    // 与Shiro不同,在Security中,并没有区分角色和权限
    List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    GrantedAuthority grantedAuthority = new SimpleGrantedAuthority("ROLE_admin");
    grantedAuthorities.add(grantedAuthority);
    return new org.springframework.security.core.userdetails.User(userName,"$2a$10$gDCkllHpktQfHgwYWKW2T.JCgkUZcTZTLfDBhlJvTLO/BDSMeA2YS", grantedAuthorities);
  }
}

模拟验证码校验

import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author Mr.css
 * @date 2020-05-10 1:14
 */
public class BeforeLoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  private String servletPath;

  public BeforeLoginAuthenticationFilter(String servletPath, String failureUrl) {
    super(servletPath);
    this.servletPath = servletPath;
    setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler(failureUrl));
  }

  @Override
  public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
    return null;
  }

  /**
   * 这里模拟客户端的验证码,只要验证码是test,即可通过校验
   */
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest req = (HttpServletRequest) request;
    if(servletPath.equals(req.getServletPath()) && "POST".equalsIgnoreCase(req.getMethod())){
        if (!"test".equals(req.getParameter("token"))) {
        unsuccessfulAuthentication(req, (HttpServletResponse) response, new InsufficientAuthenticationException("输入的验证码不正确"));
        return;
      }
    }
    chain.doFilter(request, response);
  }
}

登录页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
自定义表单验证:
<form action="/user/login" method="post">
  <br/>
  用户名:
  <input type="text" name="username" placeholder="name"><br/>
  密码:
  <input type="password" name="password" placeholder="password"><br/>
  <input type="text" name="token" value="test"><br/>
  <input name="submit" type="submit" value="提交">
</form>
</body>
</html>

一些其它配置:

//单用户登录,如果有一个登录了,同一个用户在其他地方登录将前一个剔除下线
//http.sessionManagement().maximumSessions(1).expiredSessionStrategy(expiredSessionStrategy());

//单用户登录,如果有一个登录了,同一个用户在其他地方不能登录
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);

默认的登录过滤器:
UsernamePasswordAuthenticationFilter

原文地址:https://www.cnblogs.com/chenss15060100790/p/12926660.html