springsecurity01-springsecurity架构和入门

springsecurity简介

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security为基于J2EE企业应用软件提供了全面安全服务。 特别是使用领先的J2EE解决方案-spring框架开发的企业软件项目。

Spring Security安全包括两个主要操作, “认证”和“验证”(或权限控制)。 这就是Spring Security面向的两个主要方向。“认证” 是为用户建立一个他所声明的主体的过程, (“主体”一般是指用户,设备或可以在你系统中执行行动的其他系统)。 “验证”指的一个用户能否在你的应用中执行某个操作。 在到达授权判断之前,身份的主体已经由身份验证过程建立了。 这些概念是通用的,不是Spring Security特有的。

在身份验证层面,Spring Security广泛支持各种身份验证模式。 这些验证模型绝大多数都由第三方提供,或正在开发的有关标准机构提供的,例如Internet Engineering Task Force。 作为补充,Spring Security也提供了自己的一套验证功能。 Spring Security目前支持认证一体化和如下认证技术:

  • HTTP BASIC authentication headers (一个基于IETF RFC的标准)
  • HTTP Digest authentication headers (一个基于IETF RFC的标准)
  • HTTP X.509 client certificate exchange (一个基于IETF RFC的标准)
  • LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)
  • Form-based authentication (提供简单用户接口的需求)
  • OpenID authentication
  • 基于预先建立的请求头进行认证 (比如Computer Associates Siteminder)
  • JA-SIG Central Authentication Service (也被称为CAS,这是一个流行的开源单点登录系统)
  • Transparent authentication context propagation for Remote Method Invocation (RMI) and HttpInvoker (一个Spring远程调用协议)
  • Automatic “remember-me” authentication (这样你可以设置一段时间,避免在一段时间内还需要重新验证)
  • Anonymous authentication (允许未认证的任何调用,自动假设一个特定的安全主体)
  • Run-as authentication (这在一个会话内使用不同安全身份的时候是非常有用的)
  • Java Authentication and Authorization Service (JAAS) 等

核心组件

以下通过系统化的讲解security中比较核心的几个关键对象

SecurityContextHolder

SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到ThreadLocal 也就意味着,这是一种与线程绑定的策略。Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息。但这一切的前提,是你在web场景下使用Spring Security,而如果是Swing界面,Spring也提供了支持,SecurityContextHolder的策略则需要被替换,鉴于我的初衷是基于web来介绍Spring Security,所以这里以及后续,非web的相关的内容都一笔带过。

获取当前用户的信息
因为身份信息是与线程绑定的,所以可以在程序的任何地方使用静态方法获取用户信息。一个典型的获取当前登录用户的姓名的例子如下所示:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
String username = ((UserDetails)principal).getUsername();
} else {
String username = principal.toString();
}

getAuthentication()返回了认证信息,再次getPrincipal()返回了身份信息,UserDetails便是Spring对身份信息封装的一个接口。Authentication和UserDetails的介绍在下面的小节具体讲解,本节重要的内容是介绍SecurityContextHolder这个容器。

Authentication

先看看这个接口的源码长什么样:

package org.springframework.security.core;// <1>
public interface Authentication extends Principal, Serializable { // <1>
    Collection<? extends GrantedAuthority> getAuthorities(); //  获取当前用户的所有授权(角色子类SimpleGrantedAuthority)
    Object getCredentials();// 通常是密码
    Object getDetails();// 额外信息比如ip地址,证书等
    Object getPrincipal();// 通常是用户名
    boolean isAuthenticated();// 是否认证
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}

spring Security是如何完成身份认证的?

  1. 用户名和密码被过滤器获取到,封装成Authentication,通常情况下是UsernamePasswordAuthenticationToken实现类。

  2. AuthenticationManager 身份管理器负责验证这个Authentication

  3. 认证成功后,AuthenticationManager身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除)Authentication实例。

  4. SecurityContextHolder安全上下文容器将第3步填充了信息的Authentication,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

这是一个抽象的认证流程,而整个过程中,如果不纠结于细节,其实只剩下一个AuthenticationManager 是我们没有接触过的了,这个身份管理器我们在后面的小节介绍。将上述的流程转换成代码,便是如下的流程:

public class AuthenticationExample {
private static AuthenticationManager am = new SampleAuthenticationManager();
public static void main(String[] args) throws Exception {
    BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
    while(true) {
    System.out.println("Please enter your username:");
    String name = in.readLine();
    System.out.println("Please enter your password:");
    String password = in.readLine();
    try {
        Authentication request = new UsernamePasswordAuthenticationToken(name, password);
        Authentication result = am.authenticate(request);
        SecurityContextHolder.getContext().setAuthentication(result);
        break;
    } catch(AuthenticationException e) {
        System.out.println("Authentication failed: " + e.getMessage());
    }
    }
    System.out.println("Successfully authenticated. Security context contains: " +
            SecurityContextHolder.getContext().getAuthentication());
}
}
class SampleAuthenticationManager implements AuthenticationManager {
static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
static {
    AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));
}
public Authentication authenticate(Authentication auth) throws AuthenticationException {
    if (auth.getName().equals(auth.getCredentials())) {
    return new UsernamePasswordAuthenticationToken(auth.getName(),
        auth.getCredentials(), AUTHORITIES);
    }
    throw new BadCredentialsException("Bad Credentials");
}
}

AuthenticationManager

Spring Security中的AuthenticationManager,ProviderManager ,AuthenticationProvider类名相似看,很容易搞得晕头转向,稍微梳理一下就可以理解清楚它们的联系和设计者的用意。

  • AuthenticationManager(接口)是认证相关的核心接口,也是发起认证的出发点,因为在实际需求中,我们可能会允许用户使用用户名+密码登录,同时允许用户使用邮箱+密码,手机号码+密码登录,甚至,可能允许用户使用指纹登录,所以说AuthenticationManager一般不直接认证,

  • ProviderManager: AuthenticationManager接口的常用实现类ProviderManager 内部会维护一个
    List列表,存放多种认证方式,实际上这是委托者模式的应用(Delegate)。也就是说,核心的认证入口始终只有一个:AuthenticationManager,
    不同的认证方式:用户名+密码(UsernamePasswordAuthenticationToken),邮箱+密码,手机号码+密码登录则对应了三个AuthenticationProvider。熟悉shiro的朋友可以把AuthenticationProvider理解成Realm。在默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功。

只保留了关键认证部分的ProviderManager源码:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
        InitializingBean {
    // 维护一个AuthenticationProvider列表
    private List<AuthenticationProvider> providers = Collections.emptyList();
 
    public Authentication authenticate(Authentication authentication)
          throws AuthenticationException {
       Class<? extends Authentication> toTest = authentication.getClass();
       AuthenticationException lastException = null;
       Authentication result = null;
       // 依次认证
       for (AuthenticationProvider provider : getProviders()) {
          if (!provider.supports(toTest)) {
             continue;
          }
          try {
             result = provider.authenticate(authentication);
             if (result != null) {
                copyDetails(authentication, result);
                break;
             }
          }
          ...
          catch (AuthenticationException e) {
             lastException = e;
          }
       }
       // 如果有Authentication信息,则直接返回
       if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                 //移除密码
                ((CredentialsContainer) result).eraseCredentials();
            }
             //发布登录成功事件
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
       }
       ...
       //执行到此,说明没有认证成功,包装异常信息
       if (lastException == null) {
          lastException = new ProviderNotFoundException(messages.getMessage(
                "ProviderManager.providerNotFound",
                new Object[] { toTest.getName() },
                "No AuthenticationProvider found for {0}"));
       }
       prepareException(lastException, authentication);
       throw lastException;
    }
}

ProviderManager 中的List,会依照次序去认证,认证成功则立即返回,若认证失败则返回null,下一个AuthenticationProvider会继续尝试认证,如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。

到这里,如果不纠结于AuthenticationProvider的实现细节以及安全相关的过滤器,认证相关的核心类其实都已经介绍完毕了:身份信息的存放容器SecurityContextHolder,身份信息的抽象Authentication,身份认证器AuthenticationManager及其认证流程。姑且在这里做一个分隔线。下面来介绍下AuthenticationProvider接口的具体实现。

DaoAuthenticationProvider

AuthenticationProvider最最最常用的一个实现便是DaoAuthenticationProvider。顾名思义,Dao正是数据访问层的缩写,也暗示了这个身份认证器的实现思路。由于本文是一个Overview,姑且只给出其UML类图:
在这里插入图片描述
按照我们最直观的思路,怎么去认证一个用户呢?用户前台提交了用户名和密码,而数据库中保存了用户名和密码,认证便是负责比对同一个用户名,提交的密码和保存的密码是否相同便是了。在Spring Security中。提交的用户名和密码,被封装成了UsernamePasswordAuthenticationToken,而根据用户名加载用户的任务则是交给了UserDetailsService,在DaoAuthenticationProvider中,对应的方法便是retrieveUser,虽然有两个参数,但是retrieveUser只有第一个参数起主要作用,返回一个UserDetails。还需要完成UsernamePasswordAuthenticationToken和UserDetails密码的比对,这便是交给additionalAuthenticationChecks方法完成的,如果这个void方法没有抛异常,则认为比对成功。比对密码的过程,用到了PasswordEncoder和SaltSource,密码加密和盐的概念相信不用我赘述了,它们为保障安全而设计,都是比较基础的概念。

UserDetails与UserDetailsService

UserDetails这个接口,它代表了最详细的用户信息,这个接口涵盖了一些必要的用户信息字段,具体的实现类对它进行了扩展。

public interface UserDetails extends Serializable {
   Collection<? extends GrantedAuthority> getAuthorities();
   String getPassword();
   String getUsername();
   boolean isAccountNonExpired();
   boolean isAccountNonLocked();
   boolean isCredentialsNonExpired();
   boolean isEnabled();
}

它和Authentication接口很类似,比如它们都拥有username,authorities,区分他们也是本文的重点内容之一。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户正确的密码,认证器其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getUserDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider之后被填充的。

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

UserDetailsService和AuthenticationProvider两者的职责常常被人们搞混,关于他们的问题在文档的FAQ和issues中屡见不鲜。记住一点即可,敲黑板!!!UserDetailsService只负责从特定的地方(通常是数据库)加载用户信息,仅此而已,记住这一点,可以避免走很多弯路。UserDetailsService常见的实现类有JdbcDaoImpl,InMemoryUserDetailsManager,前者从数据库加载用户,后者从内存中加载用户,也可以自己实现UserDetailsService,通常这更加灵活。

架构概览图

在这里插入图片描述

springsecurity中文文档参考:http://www.mossle.com/docs/springsecurity3/html/springsecurity.html
官网:https://docs.spring.io/spring-security/site/docs/4.2.11.RELEASE/reference/htmlsingle/#get-spring-security

springboot集成 springsecurity

关于springmvc继承springsecurity参考http://www.mossle.com/docs/springsecurity3/html/ns-config.html
springboot配置参考自基于官网java配置类方式配置spring security:https://docs.spring.io/spring-security/site/docs/4.2.11.RELEASE/reference/htmlsingle/#jc
环境:

  1. html使用thymeleaf
  2. springboot项目

helloworld程序

以下编写一个最简单的helloworld程序用于入门
application.yml配置

spring:
  thymeleaf:
    cache: false
    mode: LEGACYHTML5
server:
  port: 8888

maven依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.8.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>nekohtml</groupId>
            <artifactId>nekohtml</artifactId>
            <version>1.9.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

登录成功页
添加一个控制层类

@Controller
public class TestController {
    @RequestMapping("/toSuc")
    public String suc() {
        return "suc";
    }
}

templates目录添加模板suc.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
 this is success page
</body>
</html>

java配置
添加java配置类

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
   /*
     提供一个基于内存的用户名和密码存储 使用BCryptPasswordEncoder加密
  */
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("test").password(new BCryptPasswordEncoder().encode("123456")).roles("USER").build());
        return manager;
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/test").authenticated() //指定某些路径需要认证
                    .regexMatchers("/images/.+\\.jpg").permitAll() //images目录下的图片直接不需要认证
                .anyRequest().authenticated() //除了上面的路径外都需要认证
                .and()
                .formLogin() //生成一个默认登陆页
                .permitAll() //登陆页允许直接访问不授权
               //注意 如果你访问 / 跳转到登录页面,当登录成功后会重新重定向到/ true可以设置 不跳转到之前这个页面 登录后直接到这个指定页面/toSuc
                .defaultSuccessUrl("/toSuc",true)
                .and()
                .httpBasic().and().csrf().disable(); //禁止csrf

    }
    /**
       指定使用上面的内存数据detailservice 使用bcrypt加密
   **/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
    }
}

添加测试启用类

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

效果测试
访问任意页面,比如 http://localhost:8888/test
自动跳转到内嵌的登陆页
在这里插入图片描述
输入java配置类中配置的用户名test和密码123456后,跳转到成功页
注意 java配置类的

  .defaultSuccessUrl("/toSuc",true)

如果第二个参数false,登录成功自动跳转到之前 /test页面上
true可以设置 不跳转到之前这个页面 登录后直接设置的defaultSuccessUrl上

替换登陆页

控制层添加登录页路径映射

@RequestMapping("/rlogin")
    public String rlogin() {
        return "lg"; 
    }

添加登陆页模板lg.html

<!DOCTYPE html>
<html>
<head>
    <title></title>
    <meta charset="utf-8">
    <style type="text/css">
        fieldset{width: 350px;background:#4876FF;height: 200px;margin: 150px 400px 0px}
        button{maogin: 0; padding: 0}
    </style>
</head>
<body bgcolor="#7EC0EE">
<form action="/myauth" method="post">
        <label>用户名:</label>
        <input type="text"  name="user" value="test">(请输入用户名)</br></br>
        <label>密  码:</label>
        <input type="password" name="pass"  value="123456">(输入密码)<br><br>
        <input type="submit" value="提交"><!--注意提交按钮一定不要有空的name 否则提交不带任何参数 shift-->
</form>
</body>

</html>

  • java配置类修改默认登陆页/rlogin(默认是/login)
  • 登录提交路径为/myauth(默认是/login)
  • 登录用户名参数是user(默认是username)
  • 登录密码参数是pass(默认是password)
 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/test").authenticated()
                    .regexMatchers("/images/.+\\.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                    .loginPage("/rlogin")  //登陆页路径
                    .loginProcessingUrl("/myauth") //表单提交的路径
                    .failureForwardUrl("/rlogin") //登录失败路径
                    .usernameParameter("user")
                    .passwordParameter("pass")
                    .permitAll()  //注意所有和登录相关的都要放行,不能再antMatcher那里去放行
                //注意 如果你访问 / 跳转到登录页面,当登录成功后会重新重定向到/ true可以设置 不跳转到之前这个页面 登录后直接到这个页面
                .defaultSuccessUrl("/toSuc",true)
                .and()
                .httpBasic().and().csrf().disable();

    }

重新访问测试(ok)
在这里插入图片描述

数据库验证

关于detailservice jdbc的实现是通过JdbcDaoImpl这个类 通过查看几个变量来梳理默认的权限模型

public class JdbcDaoImpl extends JdbcDaoSupport
		implements UserDetailsService, MessageSourceAware {
    //通过用户名查询用户信息的sql enabled表示用户是否启用
	public static final String DEF_USERS_BY_USERNAME_QUERY = "select username,password,enabled "
			+ "from users " + "where username = ?";
	//通过用户名查询用户所有的权限比如 ROLE_ADMIN就是admin角色
	public static final String DEF_AUTHORITIES_BY_USERNAME_QUERY = "select username,authority "
			+ "from authorities " + "where username = ?";
	//通过用户名查询组名和组权限,组就类似于角色的概念,一组用户
	public static final String DEF_GROUP_AUTHORITIES_BY_USERNAME_QUERY = "select g.id, g.group_name, ga.authority "
			+ "from groups g, group_members gm, group_authorities ga "
			+ "where gm.username = ? " + "and g.id = ga.group_id "
			+ "and g.id = gm.group_id";

mysql数据库添加表

#用户表
CREATE TABLE users(
      username VARCHAR(50) NOT NULL PRIMARY KEY,
      PASSWORD VARCHAR(200) NOT NULL,
      enabled BOOLEAN NOT NULL);
 #权限表
CREATE TABLE authorities (
      username VARCHAR(50) NOT NULL,
      authority VARCHAR(50) NOT NULL,
      CONSTRAINT fk_authorities_users FOREIGN KEY(username) REFERENCES users(username));
      CREATE UNIQUE INDEX ix_auth_username ON authorities (username,authority);     
 #分组表
CREATE TABLE groups (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  group_name VARCHAR(50) NOT NULL);
 #分组权限
CREATE TABLE group_authorities (
  group_id BIGINT NOT NULL,
  authority VARCHAR(50) NOT NULL,
  CONSTRAINT fk_group_authorities_group FOREIGN KEY(group_id) REFERENCES groups(id));
#用户分组关系表
CREATE TABLE group_members (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  group_id BIGINT NOT NULL,
  CONSTRAINT fk_group_members_group FOREIGN KEY(group_id) REFERENCES groups(id));

插入一行数据

insert into `users` (`username`, `password`, `enabled`) values('test','$2a$10$AZ6YJq3D/s9H3vHrrW8.i.IltfvU4yfEj/EtBqNZhEls54q3JgCXe','1');
#注意一定要授权,不添加无法登陆成功
insert into `authorities` (`username`, `authority`) values('test','ROLE_ADMIN');

这里密码通过加密输出

 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(8);//加盐长度
        System.out.println(bCryptPasswordEncoder.encode("654321"));

修改替换之前内存配置的UserDetailService为

  @Autowired DataSource dataSource;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        JdbcDaoImpl jdbcDao=new JdbcDaoImpl();
        jdbcDao.setDataSource(dataSource);
        auth.userDetailsService(jdbcDao).passwordEncoder(new BCryptPasswordEncoder());
    }

或者直接设置jdbc

    @Autowired PasswordEncoder passwordEncoder;
    @Bean
    public PasswordEncoder passwordEncoder(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(8);
        return bCryptPasswordEncoder;
    }
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource).passwordEncoder(passwordEncoder);
    }
}

添加maven依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

添加数据源配置application.yml

spring:
  datasource:
    username: root
    url: jdbc:mysql://localhost/test
    driver-class-name: com.mysql.jdbc.Driver
    password: 123456

最后测试登陆 成功

自定义验证

自定义验证要求实现一个UserDetailsService接口的实现类,返回UserDetails对象(包含查询出来的用户名密码和权限)
这里顺便实现一个MD5密码加密器,代码:

package io.github.jiaozi789;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

/**
 * 使用md5算法
 * @author 廖敏
 * 创建日期 2019-03-13 9:41
 **/
@Component
public class Md5PasswordEncoder implements PasswordEncoder {
    /**
     * 密码加密
     * @param rawPassword 原始密码
     * @return
     */
    @Override
    public String encode(CharSequence rawPassword) {
        try {
            MessageDigest messageDigest=MessageDigest.getInstance("MD5");
            return Base64.getEncoder().encodeToString(messageDigest.digest(rawPassword.toString().getBytes()));
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 比较原始密码和加密后的密码是否相等
     * @param rawPassword 原始密码,用户文本框输入的
     * @param encodedPassword 加密后密码,数据库提取的
     * @return
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        try {
            MessageDigest messageDigest=MessageDigest.getInstance("MD5");
            String encodPass=Base64.getEncoder().encodeToString(messageDigest.digest(rawPassword.toString().getBytes()));
            if(encodPass.equalsIgnoreCase(encodedPassword)){
                return true;
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return false;
    }
}

添加验证逻辑service
假设用户名是zs,密码是234567的md5加密base64字符串 role是ROLE_ADMIN

package io.github.jiaozi789;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
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 org.springframework.stereotype.Service;

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

/**
 * @author 廖敏
 * 创建日期 2019-03-13 9:39
 **/
@Service
public class MyUserDetailService implements UserDetailsService {
    private String userName="zs";
    private String password="UI30yy9Nj4BRklYljPuXXw==";
    private String role="ROLE_ADMIN";
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if(username.equalsIgnoreCase(userName)){
            List<GrantedAuthority> authorityList=new ArrayList<>();
            SimpleGrantedAuthority simpleGrantedAuthority=new SimpleGrantedAuthority(role);
            authorityList.add(simpleGrantedAuthority);
            User user=new User(userName,password,authorityList);
            return user;
        }
        throw new UsernameNotFoundException(username);
    }
}

修改配置类重新配置userDetailService

@Autowired
    private MyUserDetailService myUserDetailService;
    @Autowired
    private Md5PasswordEncoder md5PasswordEncoder;
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(myUserDetailService).passwordEncoder(md5PasswordEncoder);
    }

测试使用zs和234567登录是否成功

自定义登出

默认的登出路径为 /logout,在页面添加超链接登出,指定/logout,自动跳回到登陆页,清除cookie
可以通过配置设置logout页面

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/test").authenticated()
                    .regexMatchers("/images/.+\\.jpg").permitAll()
                .anyRequest().authenticated()
                .and()
                .logout().logoutUrl("/mylogout")
 }

然后定义路径映射mylogout,做一些处理

@RequestMapping(value = "/mylogout", method = RequestMethod.GET)
    public String logout(Map model) {
            model.put("msg", "你已经成功退出");
        return "lg";
    }

基于注解授权

Spring Security的声明式安全授权有两种方式,一种是以url模式匹配的方式,另一种是方法上使用注解声明权限
Spring Security默认是禁用注解的,要想开启注解,要在继承WebSecurityConfigurerAdapter的类加@EnableGlobalMethodSecurity注解,并在该类中将AuthenticationManager定义为Bean。
提供三种方法级基于注解权限控制

  • 1.securedEnabled: Spring Security自带注解
  • 2.jsr250Enabled: 基于简单角色控制的权限注解
  • 3.prePostEnabled: 基于表达式

jsr250注解

启用方式:

@EnableGlobalMethodSecurity(jsr250Enabled = true)

支持以下注解

@DenyAll拒绝所有访问。
@RolesAllowed({"USER","ADMIN"})该方法只要具有“USER","ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
@PermitAll允许所有访问

测试控制层添加方法

@DenyAll
    @ResponseBody
    @GetMapping("/test")
    public String test() {
        return "hello";
    }

    @RolesAllowed("ADMIN")
    @ResponseBody
    @GetMapping("/a")
    public String a() {
        return "hello";
    }

登录后访问/test 用于是403拒绝
登录后访问/a 发现能访问 因为之前用户被赋予了ROLE_ADMIN权限如果改成ADMIN1就会显示403了

securedEnabled

启用方式

@EnableGlobalMethodSecurity(securedEnabled = true)

@Secured注解
在方法上指定安全性要求,只有对应角色的用户才可以调用这些方法,必须和用户定义的角色完全一样 比如ROLE_也要有
测试

@Secured("ROLE_ADMIN")
    @ResponseBody
    @GetMapping("/test")
    public String test() {
        return "hello";
    }

    @Secured("ROLE_ADMIN1")
    @ResponseBody
    @GetMapping("/a")
    public String a() {
        return "hello";
    }

登录后 /test可以访问 /a无法访问

prePostEnabled注解

是一种基于表达式的注解,并可以自定义扩展,只需继承GlobalMethodSecurityConfiguration类就可以实现。当然,别忘了在该扩展类上添加注解:@EnableGlobalMethodSecurity(prePostEnabled=true)来启动这种注解支持。如果没有访问方法的权限,会抛出AccessDeniedException。

  1. @PreAuthorize
    在方法执行之前执行,而且这里可以调用方法的参数,也可以得到参数值,这里利用JAVA8的参数名反射特性,如果没有JAVA8,那么也可以利用Spring Secuirty的@P标注参数,或利用Spring Data的@Param标注参数
    比如service方法
@PreAuthorize("#userId == authentication.principal.userId or hasAuthority(‘ROLE_ADMIN’)")
Arcticle findArcticle(@P("userId") long userId ){  }
  1. @PostAuthorize
    在方法执行之后执行,而且这里可以调用方法的返回值,如果EL为false,那么该方法也已经执行完了,可能会回滚。EL变量returnObject表示返回的对象。
    比如:
@PostAuthorize("returnObject.userId == authentication.principal.userId or hasPermission(returnObject, 'ADMIN')")
User getUser();
  1. @PostFilter

在执行方法之后执行,而且这里可以调用方法的返回值,然后对返回值进行过滤或处理。EL变量returnObject表示返回的对象。只有方法返回的集合或数组类型的才可以使用。(与分页技术不兼容)

  1. @PreFilter

EL变量filterObject表示参数,如果有多个参数,可以使用@filterTarget注解参数,只有方法是集合或数组才行(与分页技术不兼容)。
目前支持的表达式

表达式描述
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。
hasIpAddress判断用户访问的ip地址 比如 hasRole(‘USER’) and hasIpAddress(‘10.10.10.3’)
hasPermission判断是某个UserDetail子类是否拥有某个权限,具体用法自行百度

比如用户的角色权限是ROLE_ADMIN 注意hasRole不需要添加前缀 hasRole(‘ADMIN’)
hasAuthority是用户拥有某项细粒度权限 比如可以设置为 USER_1_DELETE 使用表达式hasAuthority('USER_1_DELETE ')
如果是角色也必须全名匹配 hasAuthority('ROLE_ADMIN ')

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值