跳至主要內容

SpringBoot安全访问

wangdx大约 17 分钟

SpringSecurity 快速整合

1、
project('microboot-spring-security') { // 子模块
    dependencies { // 配置子模块依赖
        compile(project(':microboot-common')) // 引入其他子模块
        compile('org.springframework.boot:spring-boot-starter-security')
    }
}


2、
package com.yootk.action;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/message/*")
public class MessageAction {
    @GetMapping("show")
    public Object show() {
        return "沐言科技:www.yootk.com";
    }
}


3、
package com.yootk;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication // 一个注解解决所有的问题
public class StartSecurityApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(StartSecurityApplication.class,args); // 运行SpringBoot程序
    }
}

4、
http://localhost:8080/show

5、
spring:
  security: # SpringSecurity的相关配置
    user: # 用户配置
      name: muyan # 用户名
      password: yootk # 密码
      roles: ADMIN, USER # 用户角色配置
server:
  port: 80

基于 Bean 配置 SpringSecurity

SpringSecurity 配置

  • SpringBoot 为了便于开发者快速实现 SpringSecurity 整合,只需要引入“spring-boot-starter-security”依赖库随后通过 application.ym|配置文件即可使用,但是这样的配置方式并不适合于程序的控制,所以一般常见的做法都是基于一个配置 Bean 的方式来配置 SpringSecurity 的相关处理环境,而这个配置 Bean 需要继承“WebSecurityConfigurerAdapter”类来实现,随后覆写该类中的"confiqure(AuthenticationManagerBuilder auth)”方法,就可以实现用户认证信息的配置,本次的实现结构如图所示

密码加密处理

  • 为了保证用户认证信息的安全,一般都会对用户所使用的密码进行加密处理操作,而这时就需要通过 PasswordEncoderFactories 工厂类和 PasswordEncoder 接口来实现,例如,本次要使用的密码为“helo”,则可以采用如下的代码进行加密处理。
1、
server:
  port: 80

2、
package com.yootk.test;

import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

public class CreatePassword {
    public static void main(String[] args) {
        String password = "hello"; // 密码明文
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); // 获取PasswordEncoder接口实例
        System.out.println(encoder.encode(password));
    }
}


3、
{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae

4、
package com.yootk.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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class YootkSecurityConfig extends WebSecurityConfigurerAdapter {
    // 本次的开发暂时不基于数据库实现用户的信息管理,本次账户采用固定的密码为“hello”
    private static final String PASSWORD =
            "{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae"; // 加密后的密码
    @Bean // 如果要想使用密码,则必须配置有一个密码的编码器
    public PasswordEncoder getPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加三个账户,用户名分别为:admin、muyan、yootk
        auth.inMemoryAuthentication().withUser("admin").password(PASSWORD).roles("USER", "ADMIN");
        auth.inMemoryAuthentication().withUser("muyan").password(PASSWORD).roles("ADMIN");
        auth.inMemoryAuthentication().withUser("yootk").password(PASSWORD).roles("USER");
    }
}



5、
http://localhost/message/show

HttpSecurity

1、
package com.yootk.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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class YootkSecurityConfig extends WebSecurityConfigurerAdapter {
    // 本次的开发暂时不基于数据库实现用户的信息管理,本次账户采用固定的密码为“hello”
    private static final String PASSWORD =
       "{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae"; // 加密后的密码
    @Bean // 如果要想使用密码,则必须配置有一个密码的编码器
    public PasswordEncoder getPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加三个账户,用户名分别为:admin、muyan、yootk
        auth.inMemoryAuthentication().withUser("admin").password(PASSWORD).roles("USER", "ADMIN");
        auth.inMemoryAuthentication().withUser("muyan").password(PASSWORD).roles("ADMIN");
        auth.inMemoryAuthentication().withUser("yootk").password(PASSWORD).roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { // 来进行访问配置
        http.authorizeRequests()    // 进行授权访问请求的配置(那些路径与那些角色进行匹配)
                .antMatchers("/admin/**").hasRole("ADMIN") // ADMIN角色可以访问
                .antMatchers("/member/**").access("hasAnyRole('USER')") // USER角色可以访问
                .antMatchers("/message/**").access(
                        "hasAnyRole('ADMIN') and hasRole('USER')")
                .anyRequest().authenticated() // 请求认证访问
                .and() // 继续连接后续的其他配置项
                .formLogin().loginProcessingUrl("/yootk-login") // 登录的处理路径
                .permitAll().and() // 允许提交访问
                .csrf().disable();
    }
}

返回 Rest 认证信息

前后端分离结构

  • 默认情况下 SpringSecurity 是以单实例的方式运行的,所以用户认证与授权操作可以直接基于 Session 的方式直接实现处理。随着前后端分离技术应用的不断推广,所以就必基于 Rest 方式实现用户认证与授权处理的相关数须改变 SpringSecurity 原始应用形式据响应

Rest 数据响应

  • 在前后端分离项目设计过程中,服务端进行用户认证后需要将认证信息(主要是 SessionID)返回给当前用户这样就可以直接利用本地化存储方式实现认证数据的保存,同时在每次进行资源调用时都需要将此认证信息传递到服务器端,以便于服务端的认证与授权检测(依据发送请求时携带的 SessionID 实现认证与授权的检测处理),这样一来就需要修改 SpringSecurity 中的数据响应处理操作,可以依靠 HttpSecurity 类提供的处理方法手工实现 Rest 数据响应
1、
package com.yootk.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class YootkSecurityConfig extends WebSecurityConfigurerAdapter {
    // 本次的开发暂时不基于数据库实现用户的信息管理,本次账户采用固定的密码为“hello”
    private static final String PASSWORD =
            "{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae"; // 加密后的密码

    @Bean // 如果要想使用密码,则必须配置有一个密码的编码器
    public PasswordEncoder getPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加三个账户,用户名分别为:admin、muyan、yootk
        auth.inMemoryAuthentication().withUser("admin").password(PASSWORD).roles("USER", "ADMIN");
        auth.inMemoryAuthentication().withUser("muyan").password(PASSWORD).roles("ADMIN");
        auth.inMemoryAuthentication().withUser("yootk").password(PASSWORD).roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { // 来进行访问配置
        http.authorizeRequests()    // 进行授权访问请求的配置(那些路径与那些角色进行匹配)
                .antMatchers("/admin/**").hasRole("ADMIN") // ADMIN角色可以访问
                .antMatchers("/member/**").access("hasAnyRole('USER')") // USER角色可以访问
                .antMatchers("/message/**").access(
                "hasAnyRole('ADMIN') and hasRole('USER')")
                .anyRequest().authenticated() // 请求认证访问
                .and() // 继续连接后续的其他配置项
                .formLogin().loginProcessingUrl("/yootk-login") // 登录的处理路径
                .usernameParameter("uname").passwordParameter("upass") // 认证的参数名称
                .permitAll()
                .successHandler(new AuthenticationSuccessHandler() { // 实现认证成功之后的配置处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal(); // 认证以及授权的内容
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_OK); // 响应200的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_OK); // 当前的登录状态
                        result.put("message", "用户登录成功");
                        result.put("principal", principal); // 实际的开发中对于认证数据肯定要筛选
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(request)); // Map集合转为JSON数据
                    }
                }).failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 响应401的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_UNAUTHORIZED); // 当前的登录状态
                        result.put("principal", null); // 返回一个空的认证数据
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        if (exception instanceof LockedException) {
                            result.put("message", "账户被锁定,登录失败!");
                        } else if (exception instanceof BadCredentialsException) {
                            result.put("message", "账户名或密码输入错误,登录失败!");
                        } else if (exception instanceof DisabledException) {
                            result.put("message", "账户被禁用,登录失败!");
                        } else if (exception instanceof AccountExpiredException) {
                            result.put("message", "账户已过期,登录失败!");
                        } else if (exception instanceof CredentialsExpiredException) {
                            result.put("message", "密码已过期,登录失败!");
                        } else {
                            result.put("message", "未知原因,导致登录失败!");
                        }
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(request)); // Map集合转为JSON数据
                    }
                }).and().logout().logoutUrl("/yootk-logout") // 注销处理
                .clearAuthentication(true) // 清除掉所有的认证信息
                .invalidateHttpSession(true) // 注销当前的Session
                .logoutSuccessHandler(new LogoutSuccessHandler() {// 注销成功之后返回的数据内容
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal(); // 认证以及授权的内容
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_OK); // 响应200的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_OK); // 当前的登录状态
                        result.put("message", "用户注销成功");
                        result.put("principal", null); // 实际的开发中对于认证数据肯定要筛选
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(request)); // Map集合转为JSON数据
                    }
                })
                .and().csrf().disable();
    }
}


2、
curl -X POST -d "uname=admin&upass=hello" "http://localhost/yootk-login"

3、
curl -X GET "http://localhost/message/show"


4、
curl -X GET -b "JSESSIONID=4CEB2C239440A425F5FC7AEAF85EDFF4;" "http://localhost/message/show"

5、
curl -X GET -b "JSESSIONID=4CEB2C239440A425F5FC7AEAF85EDFF4;" "http://localhost/yootk-logout"

UserDetailsService

认证与授权数据

  • SpringSecurity 安全框架是基于认证与授权两种方式实现系统资源的保护,为了便于认证与授权信息的管理,在 Java 程序的设计与开发过程之中,往往会采用如图所示的类结构实现数据存储,通过 Member 类保存相关的认证数据,并且通过 Role 类保存相关的授权数据,同时根据一对多的设计原则,在 Member 类中需要提供有一个角色集合,这样只要获取了 Member 类的对象实例就可以获取到其对应的授权信息。

UserDetaisService

  • 在 SpringSecurity 框架设计时,充分的考虑到了用户对于面向对象程序设计的要求,所以提供了 UserDetails 认证数据接口(Member 实现)以及 GrantedAuthority 授权数据随后可以通过 UserDetailsService 业务接口实现认证与授权信息加接口(Role 实现)载的业务封装,最终再通过自定义的 SpringSecurity 配置类实现整合
1、
package com.yootk.vo;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
@Data
public class Role implements GrantedAuthority {// 保存授权信息
    private String rid; // 保存角色ID(一般都是字符串)
    private String title; // 保存角色的名称(仅仅是为了一些标注)
    @Override
    public String getAuthority() {
        return this.rid;
    }
}


2、
package com.yootk.vo;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

@Data
public class Member implements UserDetails { // 保存认证数据信息
    private String mid; // 用户ID
    private String name; // 用户姓名
    private String password; // 用户密码
    private Integer enabled; // 用户是否启用(1:true、0:false)
    private transient List<Role> roles; // 保存全部的角色
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles; // 返回全部的授权数据
    }

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

    @Override
    public String getUsername() { // SpringSecurity提供的用户名
        return this.mid; // 本次是通过mid的属性保存用户ID
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enabled == 1;
    }
}


3、
package com.yootk.service;

import com.yootk.vo.Member;
import com.yootk.vo.Role;
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.Arrays;

@Service
public class UserDetailsServiceImpl implements UserDetailsService { // 实现全部的认证与授权数据加载
    // 本次的开发暂时不基于数据库实现用户的信息管理,本次账户采用固定的密码为“hello”
    private static final String PASSWORD =
            "{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae"; // 加密后的密码
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 以下的操作暂时使用固定的数据项进行处理,随后将其再修改为基于数据库的开发
        if (!"admin".equals(username)) {    // 用户输入的用户名会传递过来
            throw new UsernameNotFoundException("用户信息不存在。"); // 这个时候会自动的触发登录异常
        }
        Member member = new Member(); // UserDetails接口的子类
        member.setMid("admin"); // 固定的账户信息
        member.setPassword(PASSWORD); // 加密后的密码数据
        member.setName("沐言科技管理员"); // 纯粹是一个打酱油的信息
        member.setEnabled(1);// 该账户为启用状态
        Role roleAdmin = new Role(); // 用户的角色数据
        roleAdmin.setRid("ROLE_ADMIN"); // 必须使用“ROLE_”开头
        roleAdmin.setTitle("管理员"); // 【打酱油的信息】仅仅做为一个标注
        Role roleUser = new Role(); // 用户的角色数据
        roleUser.setRid("ROLE_USER"); // 必须使用“ROLE_”开头
        roleUser.setTitle("用户"); // 【打酱油的信息】仅仅做为一个标注
        // 所有的角色数据一定要与UserDetails进行匹配
        member.setRoles(Arrays.asList(roleAdmin, roleUser)); // 用户的角色数据
        return member;
    }
}


4、
package com.yootk.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class YootkSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService; // 注入所需要的实例
    @Bean // 如果要想使用密码,则必须配置有一个密码的编码器
    public PasswordEncoder getPasswordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(this.userDetailsService); // 通过UserDetailsService查询
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception { // 来进行访问配置
        http.authorizeRequests()    // 进行授权访问请求的配置(那些路径与那些角色进行匹配)
                .antMatchers("/admin/**").hasRole("ADMIN") // ADMIN角色可以访问
                .antMatchers("/member/**").access("hasAnyRole('USER')") // USER角色可以访问
                .antMatchers("/message/**").access(
                "hasAnyRole('ADMIN') and hasRole('USER')")
                .anyRequest().authenticated() // 请求认证访问
                .and() // 继续连接后续的其他配置项
                .formLogin().loginProcessingUrl("/yootk-login") // 登录的处理路径
                .usernameParameter("uname").passwordParameter("upass") // 认证的参数名称
                .permitAll()
                .successHandler(new AuthenticationSuccessHandler() { // 实现认证成功之后的配置处理
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal(); // 认证以及授权的内容
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_OK); // 响应200的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_OK); // 当前的登录状态
                        result.put("message", "用户登录成功");
                        result.put("principal", principal); // 实际的开发中对于认证数据肯定要筛选
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(result)); // Map集合转为JSON数据
                    }
                }).failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 响应401的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_UNAUTHORIZED); // 当前的登录状态
                        result.put("principal", null); // 返回一个空的认证数据
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        if (exception instanceof LockedException) {
                            result.put("message", "账户被锁定,登录失败!");
                        } else if (exception instanceof BadCredentialsException) {
                            result.put("message", "账户名或密码输入错误,登录失败!");
                        } else if (exception instanceof DisabledException) {
                            result.put("message", "账户被禁用,登录失败!");
                        } else if (exception instanceof AccountExpiredException) {
                            result.put("message", "账户已过期,登录失败!");
                        } else if (exception instanceof CredentialsExpiredException) {
                            result.put("message", "密码已过期,登录失败!");
                        } else {
                            result.put("message", "未知原因,导致登录失败!");
                        }
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(result)); // Map集合转为JSON数据
                    }
                }).and().logout().logoutUrl("/yootk-logout") // 注销处理
                .clearAuthentication(true) // 清除掉所有的认证信息
                .invalidateHttpSession(true) // 注销当前的Session
                .logoutSuccessHandler(new LogoutSuccessHandler() {// 注销成功之后返回的数据内容
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        Object principal = authentication.getPrincipal(); // 认证以及授权的内容
                        response.setContentType("application/json;charset=UTF-8"); // 响应的类型为JSON
                        response.setStatus(HttpServletResponse.SC_OK); // 响应200的状态码
                        Map<String, Object> result = new HashMap<>(); // 登录成功之后响应数据
                        result.put("status", HttpServletResponse.SC_OK); // 当前的登录状态
                        result.put("message", "用户注销成功");
                        result.put("principal", null); // 实际的开发中对于认证数据肯定要筛选
                        result.put("sessionId", request.getSession().getId()); // 所有的认证路径检测都通过SessionID进行
                        // 此时需要将Map集合转为JSON结构,按照Spring默认的转换建议使用Jackson工具
                        ObjectMapper mapper = new ObjectMapper();
                        response.getWriter().println(mapper.writeValueAsString(result)); // Map集合转为JSON数据
                    }
                })
                .and().csrf().disable();
    }
}


5、
http://localhost/message/show

基于数据库实现认证授权

整合数据库认证

  • 实际项目开发之中,会存在有大量的用户信息,所以此时最佳的做法是将用户的认证与授权信息通过关系型数据库进行存储,这样在每次进行登录认证时就可以通过数据库实现相关数据的加载

数据表与 JPA 映射

  • H-个用户存在有多个角色,一个角色会有多个用户,这样就形成了一个完整的多对多关联,所以本次的项目开发需要准备三张数据表:用户表(member)、角色表(roe)、用户-角色关系表(member role)
1、
-- 删除数据库
DROP DATABASE IF EXISTS springsecurity ;
-- 创建数据库
CREATE DATABASE springsecurity DEFAULT CHARACTER SET utf8 ;
-- 使用数据库
USE springsecurity ;
-- 创建用户表(mid:登录ID、name:真实姓名、password:登录密码、enabled:启用状态)
-- enabled取值有两种:启用(enabled=1)、锁定(enabled=0)
CREATE TABLE member(
   mid			VARCHAR(50)    ,
   name			VARCHAR(50) ,
   password		VARCHAR(68)    ,
   enabled		INT(1) ,
   CONSTRAINT pk_mid PRIMARY KEY(mid)
) engine=innodb ;
-- 创建角色表(rid:角色ID,也是授权检测的名称、title:角色名称)
CREATE TABLE role (
   rid			VARCHAR(50) ,
   title			VARCHAR(50) ,
   CONSTRAINT pk_rid PRIMARY KEY(rid)
) engine=innodb ;
-- 创建用户角色关联表(mid:用户ID、rid:角色ID)
CREATE TABLE member_role (
   mid			VARCHAR(50) ,
   rid			VARCHAR(50) ,
   CONSTRAINT fk_mid FOREIGN KEY(mid) REFERENCES member(mid) ON DELETE CASCADE ,
   CONSTRAINT fk_rid FOREIGN KEY(rid) REFERENCES role(rid) ON DELETE CASCADE
) engine=innodb ;
-- 增加用户数据(admin/hello、muyan/hello、yootk/hello)
INSERT INTO member(mid,name,password,enabled) VALUES ('admin','李兴华',
	'{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae',1) ;
INSERT INTO member(mid,name,password,enabled) VALUES ('muyan','沐言科技',
	'{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae',0) ;
INSERT INTO member(mid,name,password,enabled) VALUES ('yootk','沐言优拓',
	'{bcrypt}$2a$10$2ddAwTKN4ZZ8cNB1YgQmNeOqSLcqcTNDOF0hAxQkRWBIij1XlMvae',1) ;
-- 增加角色数据
INSERT INTO role(rid,title) VALUES ('ROLE_ADMIN','管理员') ;
INSERT INTO role(rid,title) VALUES ('ROLE_USER','用户') ;
-- 增加用户与角色信息
INSERT INTO member_role(mid,rid) VALUES ('admin','ROLE_ADMIN') ;
INSERT INTO member_role(mid,rid) VALUES ('admin','ROLE_USER') ;
INSERT INTO member_role(mid,rid) VALUES ('muyan','ROLE_ADMIN') ;
INSERT INTO member_role(mid,rid) VALUES ('yootk','ROLE_USER') ;
-- 提交事务
COMMIT ;


2、
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.5.1'


3、
project('microboot-spring-security') { // 子模块
    dependencies { // 配置子模块依赖
        compile(project(':microboot-common')) // 引入其他子模块
        compile('org.springframework.boot:spring-boot-starter-security')
        compile(libraries.'mysql-connector-java')
        compile(libraries.'druid-spring-boot-starter')
        compile('org.springframework.boot:spring-boot-starter-data-jpa')
    }
}

4、
package com.yootk.vo;

import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.Collection;
import java.util.List;

@Data
@Entity
@Table // 表名称与类名称相同,所以此处就简单编写注解了
public class Member implements UserDetails { // 保存认证数据信息
    @Id
    private String mid; // 用户ID
    private String name; // 用户姓名
    private String password; // 用户密码
    private Integer enabled; // 用户是否启用(1:true、0:false)
    @ManyToMany(targetEntity = Role.class) // 定义多对多的配置类
    @JoinTable(
            name = "member_role", // 定义表名称
            joinColumns = {@JoinColumn(name = "mid")}, // 两张表依靠mid字段进行关联
            inverseJoinColumns = {@JoinColumn(name = "rid")}
    ) // 进行中间关联表的配置
    @JsonBackReference // 防止Jacks组件在输出的时候进行递归调用
    private List<Role> roles; // 保存全部的角色
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.roles; // 返回全部的授权数据
    }

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

    @Override
    public String getUsername() { // SpringSecurity提供的用户名
        return this.mid; // 本次是通过mid的属性保存用户ID
    }

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

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

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

    @Override
    public boolean isEnabled() {
        return this.enabled == 1;
    }
}


5、
package com.yootk.vo;

import com.fasterxml.jackson.annotation.JsonBackReference;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import java.util.List;

@Data
@Entity
@Table
public class Role implements GrantedAuthority {// 保存授权信息
    @Id
    private String rid; // 保存角色ID(一般都是字符串)
    private String title; // 保存角色的名称(仅仅是为了一些标注)
    @ManyToMany(mappedBy = "roles") // Member类中的属性
    @JsonBackReference // 防止Jacks组件在输出的时候进行递归调用
    private List<Member> members; // 必须按照此类方式进行设置
    @Override
    public String getAuthority() {
        return this.rid;
    }
}


6、
package com.yootk.dao;

import com.yootk.vo.Member;
import org.springframework.data.jpa.repository.JpaRepository;
public interface IMemberDAO extends JpaRepository<Member, String> {
}


7、
server:
  port: 80
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springsecurity
    username: root
    password: mysqladmin
  jpa:
    show-sql: true
    properties:
      hibernate: # 添加JPA的属性
        enable_lazy_load_no_trans: true # 配置延迟加载

8、
package com.yootk.service;

import com.yootk.dao.IMemberDAO;
import com.yootk.vo.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserDetailsServiceImpl implements UserDetailsService { // 实现全部的认证与授权数据加载
    @Autowired
    private IMemberDAO memberDAO; // 注入JPA接口
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> optional = this.memberDAO.findById(username); // 根据ID查询数据
        if (optional.isEmpty()) {   // 此时无法获取到数据信息
            throw new UsernameNotFoundException("用户信息不存在!");
        }
        return optional.get(); // 获取认证与授权数据
    }
}


9、
package com.yootk;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories("com.yootk.dao") // 配置扫描包
@EntityScan("com.yootk.vo") // 配置实体类扫描包
@SpringBootApplication // 一个注解解决所有的问题
public class StartSecurityApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(StartSecurityApplication.class,args); // 运行SpringBoot程序
    }
}

demo


上次编辑于: