前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >权限框架 | 学会Spring Security权限框架,就是这么简单

权限框架 | 学会Spring Security权限框架,就是这么简单

作者头像
码神联盟
发布2019-07-31 10:29:40
4.3K0
发布2019-07-31 10:29:40
举报
文章被收录于专栏:码神联盟

简介

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架(简单说是对访问权限进行控制 )。

它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IOC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

应用的安全性包括:用户认证(Authentication)用户授权(Authorization)两个部分

用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统 。

  • 用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程

用户授权:验证某个用户是否有权限执行某个操作

  • 在一个系统中,不同用户所具有的权限是不同的
  • 比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改
  • 一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限

框架搭建

环境要求: SpringBoot、SpringSecurity、MySQL

工程搭建:

  • 创建一个SpringBoot项目,并引入相关包
代码语言:javascript
复制

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 构建启动类
代码语言:javascript
复制
@SpringBootApplication
public class SpringsecuritydemoApplication {

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

}
  • 启动项目,访问即可:http://localhost:8080
  • 输入用户名和密码
    • 用户名:user
    • 密码:可以通过启动控制台查看
    • Using generated security password

框架分析

浏览器,访问http://localhost:8080,发现会跳转到第一个登录页面,Spring Security已经默认做了一些配置,并且创建一个简单的登录页面 ,那这个页面是怎么来的?通过跟踪源码来一探究竟。

Spring Security 官网文档链接:

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#jc-oauth2login

跳转登录页面

通过查看文档发现,WebSecurityConfigurerAdapter 提供的默认的配置 ,这个抽象类中,提供了一个方法formLogin(),内容如下:

代码语言:javascript
复制
protected void configure(HttpSecurity http) throws Exception {
        logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");

        http
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            .formLogin().and()
            .httpBasic();
    }
代码语言:javascript
复制
public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
        return getOrApply(new FormLoginConfigurer<>());
    }

查看formLogin()源码,跳转到HttpSecurity类中,这个方法返回一个 FormLoginConfigurer<HttpSecurity>类型,再继续来看这个FormLoginConfigurer,在FormLoginConfigurer中有个initDefaultLoginFilter()方法

代码语言:javascript
复制
private void initDefaultLoginFilter(H http) {
        DefaultLoginPageGeneratingFilter loginPageGeneratingFilter = http
                .getSharedObject(DefaultLoginPageGeneratingFilter.class);
        if (loginPageGeneratingFilter != null && !isCustomLoginPage()) {
            loginPageGeneratingFilter.setFormLoginEnabled(true);
            loginPageGeneratingFilter.setUsernameParameter(getUsernameParameter());
            loginPageGeneratingFilter.setPasswordParameter(getPasswordParameter());
            loginPageGeneratingFilter.setLoginPageUrl(getLoginPage());
            loginPageGeneratingFilter.setFailureUrl(getFailureUrl());
            loginPageGeneratingFilter.setAuthenticationUrl(getLoginProcessingUrl());
        }
    }

这个方法,初始化一个默认登录页的过滤器,可以看到第一句代码,默认的过滤器是DefaultLoginPageGeneratingFilter ,进入过滤器中 :

代码语言:javascript
复制
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        boolean loginError = isErrorPage(request);
        boolean logoutSuccess = isLogoutSuccess(request);
        if (isLoginUrlRequest(request) || loginError || logoutSuccess) {
            String loginPageHtml = generateLoginPageHtml(request, loginError,
                    logoutSuccess);
            .........

            return;
        }

        chain.doFilter(request, response);
    }

可以看到,如果没有配置login页,这个过滤器会被创建,然后看doFilter()方法,登录页面的配置是通过generateLoginPageHtml()方法创建的,再来看这个方法 :

代码语言:javascript
复制
private String generateLoginPageHtml(HttpServletRequest request, boolean loginError,
            boolean logoutSuccess) {
        String errorMsg = "Invalid credentials";

        if (loginError) {
            HttpSession session = request.getSession(false);

            if (session != null) {
                AuthenticationException ex = (AuthenticationException) session
                        .getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
                errorMsg = ex != null ? ex.getMessage() : "Invalid credentials";
            }
        }

        StringBuilder sb = new StringBuilder();

        sb.append("<!DOCTYPE html>\n"
                + "<html lang=\"en\">\n"
                + "  <head>.......");

        String contextPath = request.getContextPath();
        if (this.formLoginEnabled) {
            sb.append("      <form class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.authenticationUrl + "\">\n"
                    + "........");
        }

        if (openIdEnabled) {
            sb.append("      <form name=\"oidf\" class=\"form-signin\" method=\"post\" action=\"" + contextPath + this.openIDauthenticationUrl + "\">\n"
                  .......
        }

        if (oauth2LoginEnabled) {
            sb.append("<h2 class=\"form-signin-heading\">Login with OAuth 2.0</h2>");
           .......
            for (Map.Entry<String, String> clientAuthenticationUrlToClientName : oauth2AuthenticationUrlToClientName.entrySet()) {
72                    .........
            }
            sb.append("</table>\n");
        }
        sb.append("</div>\n");
        sb.append("</body></html>");

        return sb.toString();
    }

至此,默认登录页及配置,已经可以清楚了 。

用户名和密码分析

在项目启动的日志中,可以发现有这样一条信息 :

代码语言:javascript
复制
2019-05-20 16:26:24.846  INFO 9032 --- [           main] .s.s.UserDetailsServiceAutoConfiguration : 

Using generated security password: 5baa53a8-2ff7-4fea-9e10-185b5f640dad

可以看到,自动配置类是UserDetailsServiceAutoConfiguration,密码是 :5baa53a8-2ff7-4fea-9e10-185b5f640dad,现在知道了密码,那用户名是什么还不知道,进入到 UserDetailsServiceAutoConfiguration去看看:

在这个 UserDetailsServiceAutoConfiguration 类的描述中可以知道,这个类是设置一些 Spring Security 相关默认的自动配置,把InMemoryUserDetailsManager 中得user 和 password 信息设置为默认得用户和密码,可以通过提供的AuthenticationManager、AuthenticationProvider 或者 UserDetailsService 的 bean 来覆盖默认的自动配置信息:

代码语言:javascript
复制
@Bean
    @ConditionalOnMissingBean(
            type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
    @Lazy
    public InMemoryUserDetailsManager inMemoryUserDetailsManager(
            SecurityProperties properties,
            ObjectProvider<PasswordEncoder> passwordEncoder) {
        SecurityProperties.User user = properties.getUser();
        List<String> roles = user.getRoles();
        return new InMemoryUserDetailsManager(User.withUsername(user.getName())
                .password(getOrDeducePassword(user,  passwordEncoder.getIfAvailable()))
                .roles(StringUtils.toStringArray(roles)).build());
    }

private String getOrDeducePassword(SecurityProperties.User user,
            PasswordEncoder encoder) {
        String password = user.getPassword();
        if (user.isPasswordGenerated()) {
            logger.info(String.format("%n%nUsing generated security password: %s%n",
                    user.getPassword()));
        }
        if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
            return password;
        }
        return NOOP_PASSWORD_PREFIX + password;
    }

可以看到, 日志输出的密码是通过inMemoryUserDetailsManager()方法获取,返回一个新的带有UserDetials信息参数构造的InMemoryUSerDetailsManager对象 ,第一个参数为:User.withUsername(user.getName()),其中user 对象是上面SecurityProperties.User类型 的,通过SecurityProperties 对象中获取的 ,看下SecurityProperties类 :

代码语言:javascript
复制
@ConfigurationProperties(prefix = "spring.security")
public class SecurityProperties {
    private User user = new User();

    public User getUser() {
        return this.user;
    }

    public static class User {  
        private String name = "user";

        private String password = UUID.randomUUID().toString();

    }

}

通过配置文件中的,前缀为spring.security 的配置可以改变默认配置信息,再看看SecurityProperties 的 getUser()方法 ,通过一步步的跟踪,发现默认的用户名是user

框架核心过滤器

想要对WEB资源进行保护,最好的办法就是Filter,想要对方法进行保护,最好的办法就是AOP,SpringSecurity在我们进行用户认证和授权的时候,会通过各种各样的拦截器来控制权限的访问,从而实现安全。SpringSecurity常见的过滤器有:

Filter

含义

WebAsyncManagerIntegrationFilter

异步 , 提供了对securityContext和WebAsyncManager的集成

SecurityContextPersistenceFilter

同步 , 从配置的SecurityContextRepository而不是request中获取信息存到SecurityContextHolder,并且当请求结束清理contextHolder时将值存回repository中(默认使用HttpSessionSecurityContextRepository).在该过滤器中每一个请求仅执行一次,该filter需在任何认证处理机制其作用之前执行。认证处理机制如basic,cas等期望在执行时从SecurityContextHolder中获取SecurityContext

HeaderWriterFilter

是一个向HttpServletResponse写入http请求头的约定

CsrfFilter

通过使用同步token模式来进行csrf防护

LogoutFilter

记录用户的退出

RequestCacheAwareFilter

用于用户登录成功后,重新恢复因为登录被打断的请求 , 请求信息被保存到cache中

SecurityContextHolderAwareRequestFilter

包装请求对象request

AnonymousAuthenticationFilter

是在UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter这些过滤器后面的,所以如果这三个过滤器都没有认证成功,则为当前的SecurityContext中添加一个经过匿名认证的token,但是通过servlet的getRemoteUser等方法是获取不到登录账号的。因为SecurityContextHolderAwareRequestFilter过滤器在AnonymousAuthenticationFilter前面

SessionManagementFilter

管理session

ExceptionTranslationFilter

处理过滤器链抛出的所有AccessDeniedException和AuthenticationException异常

FilterSecurityInterceptor

通过实现了filter来增加http资源的安全性。这个安全拦截器需要FilterInvocationSecurityMedataSource

UsernamePasswordAuthenticationFilter

登陆用户密码验证过滤器 ,基于用户名和密码的认证逻辑

BasicAuthenticationFilter

处理一个http请求的basic认证头,将结果放入SecurityContextHolder

DefaultLoginPageGeneratingFilter

当一个用户没有配置login页面时使用。仅当跳转到login页面时用到

核心组件

Authentication

  • Authentication 是一个接口,用来表示用户认证信息的
  • 在用户登录认证之前相关信息会封装为一个Authentication 具体实现类的对象 ,在登录认证成功之后又会生成一个信息更全面,包含用户权限等信息的 Authentication 对象,然后把它保存在 SecurityContextHolder 所持有的 SecurityContext 中,供后续的程序进行调用
  • Authentication 对象不需要我们自己去创建,在与系统交互的过程中,Spring Security 会自动为我们创建相应的 Authentication 对象 ,然后赋值给当前的 SecurityContext ,但是往往我们需要在程序中获取当前用户的相关信息,比如最常见的是获取当前登录用户的用户名。在程序的任何地方,通过如下方式我们可以获取到当前用户的用户名
代码语言:javascript
复制
public String getCurrentUsername() {
      Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
      if (principal instanceof UserDetails) {
         return ((UserDetails) principal).getUsername();
      }
      if (principal instanceof Principal) {
         return ((Principal) principal).getName();
      }
      return String.valueOf(principal);
  }
  • 通过 Authentication.getPrincipal() 可以获取到代表当前用户的信息。
  • 获取当前用户的用户名是一种比较常见的需求,关于上述代码其实 Spring Security 在 Authentication 中的实现类中已经为我们做了相关实现,所以获取当前用户的用户名最简单的方式应当如下 :
代码语言:javascript
复制
public String getCurrentUsername() {
      return SecurityContextHolder.getContext().getAuthentication().getName();
   }
  • 此外,调用 SecurityContextHolder.getContext() 获取 SecurityContext 时,如果对应的 SecurityContext 不存在,则 Spring Security 将为我们建立一个空的 SecurityContext 并进行返回

SecurityContextHolder

  • SecurityContextHolder 是用来保存 SecurityContext的
  • SecurityContext 中含有当前正在访问系统的用户的详细信息
  • 默认情况下,SecurityContextHolder 将使用 ThreadLocal 来保存 SecurityContext ,这也就意味着在处于同一线程中的方法中我们可以从 ThreadLocal 中获取到当前的 SecurityContext,因为线程池的原因,如果我们每次在请求完成后都将 ThreadLocal 进行清除的话,那么我们把 SecurityContext 存放在 ThreadLocal 中还是比较安全的
  • 这些工作 Spring Security 已经自动为我们做了,即在每一次 request 结束后都将清除当前线程的 ThreadLocal
  • SecurityContextHolder 中定义了一系列的静态方法,而这些静态方法内部逻辑基本上都是通过SecurityContextHolder 持有的 SecurityContextHolderStrategy 来实现的,如 getContext()、setContext() 、clearContext()等
  • Spring Security 还提供了两种类型的 strategy 实现,GlobalSecurityContextHolderStrategy 和 InheritableThreadLocalSecurityContextHolderStrategy ,前者表示全局使用同一个 SecurityContext;后者使用 InheritableThreadLocal 来存放 SecurityContext,即子线程可以使用父线程中存放的变量
  • 一般而言,我们使用默认的 strategy 就可以了,但是如果要改变默认的 strategy,Spring Security 为我们提供了两种方法,这两种方式都是通过改变 strategyName 来实现的
  • SecurityContextHolder 中为三种不同类型的 strategy 分别命名为 MODE_THREADLOCAL、MODE_INHERITABLETHREADLOCAL 和 MODE_GLOBAL ,第一种方式是通过 SecurityContextHolder 的静态方法 setStrategyName() 来指定需要使用的 strategy;第二种方式是通过系统属性进行指定,其中属性名默认为 “spring.security.strategy”,属性值为对应 strategy 的名称

AuthenticationManager

AuthenticationProvider

  • AuthenticationManager 是一个用来处理认证(Authentication)请求的接口
  • 认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider
  • 在其中只定义了一个方法 authenticate(),该方法只接收一个代表认证请求的 Authentication 对象作为参数,如果认证成功,则会返回一个封装了当前用户权限等信息的 Authentication 对象进行返回
代码语言:javascript
复制
Authentication authenticate(Authentication authentication) throws AuthenticationException;
  • 在 Spring Security 中,AuthenticationManager 的默认实现是 ProviderManager而且它不直接自己处理认证请求,而是委托给其所配置的 AuthenticationProvider 列表,然后会依次使用每一个 AuthenticationProvider 进行认证
  • 如果有一个 AuthenticationProvider 认证后的结果不为 null,则表示该 AuthenticationProvider 已经认证成功,之后的 AuthenticationProvider 将不再继续认证。然后直接以该 AuthenticationProvider 的认证结果作为 ProviderManager 的认证结果
  • 如果所有的 AuthenticationProvider 的认证结果都为 null,则表示认证失败,将抛出一个 ProviderNotFoundException
  • 校验认证请求最常用的方法是根据请求的用户名加载对应的 UserDetails,然后比对 UserDetails 的密码与认证请求的密码是否一致,一致则表示认证通过
  • Spring Security 内部的 DaoAuthenticationProvider 就是使用的这种方式。其内部使用 UserDetailsService 来负责加载 UserDetails ,在认证成功以后会使用加载的 UserDetails 来封装要返回的 Authentication 对象,加载的 UserDetails 对象是包含用户权限等信息的。认证成功返回的 Authentication 对象将会保存在当前的 SecurityContext 中
  • 默认情况下,在认证成功后 ProviderManager 将清除返回的 Authentication 中的凭证信息,如密码
  • 如果你在无状态的应用中将返回的 Authentication 信息缓存起来了,那么以后你再利用缓存的信息去认证将会失败,因为它已经不存在密码这样的凭证信息了
  • 所以在使用缓存的时候你应该考虑到这个问题。一种解决办法是设置 ProviderManager 的 eraseCredentialsAfterAuthentication 属性为 false,或者想办法在缓存时将凭证信息一起缓存

UserDetailsService

  • 通过 Authentication.getPrincipal() 的返回类型是 Object,但很多情况下其返回的其实是一个 UserDetails 的实例
  • UserDetails 是 Spring Security 中一个核心的接口 ,其中定义了一些可以获取用户名、密码、权限等与认证相关的信息的方法
  • Spring Security 内部使用的 UserDetails 实现类大都是内置的 User 类,我们如果要使用 UserDetails 时也可以直接使用该类
  • 在 Spring Security 内部很多地方需要使用用户信息的时候基本上都是使用的 UserDetails,比如在登录认证的时候
  • 登录认证的时候 Spring Security 会通过 UserDetailsService 的 loadUserByUsername() 方法获取对应的 UserDetails 进行认证,认证通过后会将该 UserDetails 赋给认证通过的 Authentication 的 principal,然后再把该 Authentication 存入到 SecurityContext 中
  • 之后如果需要使用用户信息的时候就是通过 SecurityContextHolder 获取存放在 SecurityContext 中的 Authentication 的 principal
  • 通常我们需要在应用中获取当前用户的其它信息,如 Email、电话等。这时存放在 Authentication 的 principal 中只包含有认证相关信息的 UserDetails 对象可能就不能满足我们的要求了。这时我们可以实现自己的 UserDetails,在该实现类中我们可以定义一些获取用户其它信息的方法,这样将来我们就可以直接从当前 SecurityContext 的 Authentication 的 principal 中获取这些信息了
  • UserDetailsService 也是一个接口,我们也需要实现自己的 UserDetailsService 来加载我们自定义的 UserDetails 信息。然后把它指定给 AuthenticationProvider 即可
  • 另外 Spring Security 还为我们提供了 UserDetailsService 另外一个实现,InMemoryDaoImpl
  • InMemoryDaoImpl 主要是测试用的,其只是简单的将用户信息保存在内存中

GrantedAuthority

  • Authentication 的 getAuthorities() 可以返回当前 Authentication 对象拥有的权限,即当前用户拥有的权限,其返回值是一个 GrantedAuthority 类型的数组,每一个 GrantedAuthority 对象代表赋予给当前用户的一种权限
  • GrantedAuthority 是一个接口,其通常是通过 UserDetailsService 进行加载,然后赋予给 UserDetails
  • GrantedAuthority 中只定义了一个 getAuthority() 方法,该方法返回一个字符串,表示对应权限的字符串表示,如果对应权限不能用字符串表示,则应当返回 null
  • Spring Security 针对 GrantedAuthority 有一个简单实现 SimpleGrantedAuthority。该类只是简单的接收一个表示权限的字符串。Spring Security 内部的所有 AuthenticationProvider 都是使用 SimpleGrantedAuthority 来封装 Authentication 对象

认证过程梳理

  • 用户使用用户名和密码进行登录
  • Spring Security 将获取到的用户名和密码封装成一个实现了 Authentication 接口的 UsernamePasswordAuthenticationToken
  • 将上述产生的 token 对象传递给 AuthenticationManager 进行登录认证
  • AuthenticationManager 认证成功后将会返回一个封装了用户权限等信息的 Authentication 对象
  • 通过调用 SecurityContextHolder.getContext().setAuthentication(...) 将 AuthenticationManager 返回的 Authentication 对象赋予给当前的 SecurityContext
  • 在认证成功后,用户就可以继续操作去访问其它受保护的资源了,但是在访问的时候将会使用保存在 SecurityContext 中的 Authentication 对象进行相关的权限鉴定,如不存在对应的访问权限,则会返回 403 错误码

SpringBoot整合SpringSecurity

  • 新建Maven工程,并导入以下包:
代码语言:javascript
复制
<dependencies>
        <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.24</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
  • 准备数据库表结构
代码语言:javascript
复制
CREATE TABLE `user` (
  `userId` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(200) DEFAULT NULL,
  `phone` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`userId`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
  • 新建实体对象类
代码语言:javascript
复制
@Data
public class User implements UserDetails {
    private Long userId;
    private String username;
    private String password;
    private String phone;

    private List<GrantedAuthority> authorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • 配置application.xml文件
代码语言:javascript
复制
# 访问端口号
server.port=8890

# 数据库相关配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/securitydemo
spring.datasource.username=root
spring.datasource.password=123456

# MyBatis的相关配置
# 映射文件位置
mybatis.mapper-locations=classpath:mapper/*.xml
# 输出SQL执行语句
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
  • 增加前端登录和首页面
代码语言:javascript
复制
login.html:
        <form th:action="@{/login}" method="post">
            <input type="text" id="username" name="username" placeholder="手机号"/>
            <br/>
            <input type="password" id="password" name="password" placeholder="密码"/>
            <br/>
            <p th:if="${param.authError}" style="color: red">用户名或者密码错误</p>
            <br/>
            <button type="submit">登录</button>
        </form>

代码语言:javascript
复制
index.html:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页面</title>
</head>
<body>

<h3>登录成功</h3>
<br/>

<form th:action="@{/user/logout}" method="post" id="logoutForm">
    <button type="submit" form="logoutForm">注销</button>
</form>

</body>
</html>
  • 增加接口页面跳转控制器
代码语言:javascript
复制
@Controller
public class LoginController {

    @Autowired
    private IUserService userService;

    @GetMapping("/user/toLogin")
    public String toLogin(){
        return "login";
    }

    @PostMapping("/user/logout")
    public String logout(){
        return "login";
    }

}
  • 增加权限控制等类
代码语言:javascript
复制
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests()
               .antMatchers("/user/toLogin").permitAll()
               .and()
               .formLogin()
               .loginProcessingUrl("/login")
               .failureHandler(authFailHandler())
               .and();
    }

    @Autowired
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authProvider());

    }

    @Bean
    public MyAuthProvider authProvider(){
        return new MyAuthProvider();
    }

    @Bean
    public MyAuthFailHandler authFailHandler(){
        return new MyAuthFailHandler();
    }
}
代码语言:javascript
复制
public class MyAuthProvider implements AuthenticationProvider {

    @Autowired
    private IUserService userService;

    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String name = authentication.getName();//获取表单提交的用户名
        String passwordForm = (String) authentication.getCredentials();//获取表单输入的密码

        User user = userService.queryUserByName(name);
        if (user==null) {
            throw new AuthenticationCredentialsNotFoundException("authError");
        }
        if(bCryptPasswordEncoder.matches(passwordForm, user.getPassword())){
            return new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
        }else{
            throw new BadCredentialsException("authError");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return true;
    }
}
代码语言:javascript
复制
public class MyAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {


        super.setDefaultFailureUrl("/user/toLogin?"+exception.getMessage());
        super.onAuthenticationFailure(request, response, exception);
    }
}
  • 增加接口和mybatis信息
代码语言:javascript
复制
public interface IUserService {

    public User queryUserByName(String username);

}
代码语言:javascript
复制
@Service
public class UserServiceImpl implements IUserService {

    @Autowired
    UserDao userDao;

    @Override
    public User queryUserByName(String username) {
        User user = userDao.queryUserByName(username);
        return user;
    }
}
代码语言:javascript
复制

<mapper namespace="com.mysecurity.dao.UserDao">

    <select id="queryUserByName" resultType="com.mysecurity.entity.User">
        select * from user where username=#{username}
    </select>
</mapper>
代码语言:javascript
复制
public interface UserDao {

    public User queryUserByName(String username);
}
  • 启动运行、访问即可
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-07-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 码上有猿 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档