SpringSecurity安全框架学习、base64编码、JWT学习和使用

基于session的认证方式

用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,
这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,
当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

基于token的认证方式(是一个令牌,一般就是一个字符串)

用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage(浏览器的本地存储)等存储中,
每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。
可以使用Redis 存储用户信息(分布式中共享session)。
(cookie 和localStorage区别==:cookie 存的东西小,如果浏览器禁用cookie 了,就只能使用本地存储)

session和token对比

服务端要存储session信息需要占用内存资源,客户端需要支持cookie;
基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式。
如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

java的安全框架实现(主要有三种)【安全框架核心:认证(authentication)和授权(authorization)】

1、Shiro(apache开源的):轻量级的安全框架,提供认证、授权、会话管理、密码管理、缓存管理等功能。
2、Spring Security:功能比Shiro强大,更复杂,权限控制细粒度更高,对OAuth2 支持更好,与Spring 框架无缝集合,使Spring Boot 集成很快捷。
3、自己写:基于过滤器(filter)和AOP来实现,难度大,没必要。

Spring Security是声明式(注解)的安全访问控制解决方案的安全框架,利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能

SpringSecurity执行流程

当访问controller的时候,请求会被身份验证过滤器拦截到,身份验证过滤器再调用(委托给)身份验证管理器。
身份验证管理器使用身份验证程序,身份验证程序再调用用户详情服务,找到用户并使用密码编码器验证密码是否正确。
身份验证的结果一步步返回,最后返回给过滤器。
验证通过的被过滤器中写好的方式,存储到安全上下文中。

SpringSecurity执行流程

SpringSecurity入门使用

1、引入依赖
2、创建请求
3、启动程序
4、访问请求

引入依赖
创建请求
启动程序
访问请求

访问成功后,显示框架提供的默认登录页面,用户名默认是user,密码是在控制台上由UUID生成的。
访问后,框架提供的默认登录页面
登录的密码
退出页面
退出
退出

使用配置文件配置用户名和密码

缺点:只能配置一个

1、引入依赖
2、创建请求(随便创建用于测试的controller即可)
3、编写配置文件
4、启动程序
5、访问请求。

编写配置文件:
  通过配置文件配置好用户名和密码后,启动程序,用户名和密码就是配置的这个。

编写配置文件:
用户名和密码

基于内存的多用户管理(用户名和密码都存储到内存中)

这个程序的问题:
1、密码没有加密。
2、两个用户未区分权限,只要登录,所有的请求都可以访问
3、无法动态创建用户

引入依赖
创建请求(延用上面自己创建的请求用于测试)
编写配置文件(可以不加配置,后面会提到)
新建配置类
启动程序
访问请求。

新建配置类:(向Spring容器中放一个Bean,如果不自定义,就会采用默认的;自定义的话会替换掉默认的)

新建配置类:

package com.powernode.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 定义一个bean,用户详情服务接口
 * UserDetailsService:根据用户名去找用户信息,找到了就交给框架判断密码是否正确
 * UserDetails:存储的是用户的详细信息,用户名、密码、权限等
 */
@Configuration
public class MySecurityUserConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        // 创建了2个用户,有用户名、有密码、有角色
        UserDetails user1 = User.builder().username("aaa").password("123456").roles("student").build();
        UserDetails user2 = User.builder().username("aaab").password("123456").roles("teacher").build();

        // 创建在内存中的用户细节管理器
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(user1);
        manager.createUser(user2);

        return manager;
    }

    /**
     * 自定义用户必须配置密码编码器
     * NoOpPasswordEncoder 没有加密
     * 
     * TODO spring Sercurity强制要使用密码加密,当然我们也可以不加密,但是官方要求不管你是否加密,都必须配置一个密码编码(加密)器
     * 
     * NoOpPasswordEncoder是典型的单例模式:这个类只能生成一个对象【spring的那些Bean都是单例的】
     * 单例模式条件:
     * 		构造方法是私有的。
     * 		对象是唯一的
     * 
     * TODO 饥饿加载:对象一开始就创建了(如NoOpPasswordEncoder)。
     * TODO 懒加载:什么时候调用某个方法,什么时候创建单例。
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

如果配置类中重新定义了UserDetailsService配置了用户,配置文件也配置用户,配置文件的用户会失效。
配置文件中的用户失效

加密方案

密码加密一般使用散列函数,又称散列算法,哈希函数,这些函数都是单向函数(从明文到密文,反之不行)
常用的散列算法有MD5和SHA
Spring Security提供多种密码加密方案,基本上都实现了PasswordEncoder接口,官方推荐使用BCryptPasswordEncoder

BCryptPasswordEncoder使用

此处这个程序的问题:
1、两个用户未区分权限,只要登录,所有的请求都可以访问
2、无法动态创建用户

通过单元测试使用

引入依赖
创建测试类
启动测试类
观察结果

创建测试类:

package com.powernode.password;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

@Slf4j
public class BCryptPasswordEncoderTest {

    @Test
    void testBcrypt() {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        // 编码
        String encode1 = passwordEncoder.encode("123456");
        String encode2 = passwordEncoder.encode("123456");
        String encode3 = passwordEncoder.encode("123456");
        log.info(encode1);
        log.info(encode2);
        log.info(encode3);
        // 对比方法,参数1:明文 参数2:密文
        boolean result1 = passwordEncoder.matches("123456", encode1);
        boolean result2 = passwordEncoder.matches("123456", encode2);
        boolean result3 = passwordEncoder.matches("123456", encode3);
        assertTrue(result1);
        assertFalse(result2);
        assertTrue(result3);
    }
}

通过配置类使用

package com.powernode.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 定义一个bean,用户详情服务接口
 */
@Configuration
public class MySecurityUserConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        // 创建了2个用户,系统的用户
        UserDetails user1 = User.builder()
                .username("eric")
                // 使用自定义密码加密器加密用户的密码,用于和前端传入的密码加密后对比
                .password(passwordEncoder().encode("123456"))
                .roles("student")
                .build();
        UserDetails user2 = User.builder()
                .username("thomas")
                .password(passwordEncoder().encode("123456"))
                .roles("teacher")
                .build();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(user1);
        manager.createUser(user2);
        return manager;
    }

    /**
     * 自定义用户必须配置密码编码器(对前端明文进行编码)
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

查看当前登录用户信息及配置用户权限

此处这个程序的问题:
1、两个用户有权限,但是没有控制权限访问的路径,只要登录,所有的请求都可以访问
2、无法动态创建用户

1、引入依赖
2、创建请求
3、新建配置类
4、编写查看用户信息的控制器(和前面的CurrentLoginUserController一样)
5、启动程序
6、访问控制器中对应的路径的请求查看权限

配置类配置用户权限和角色:
权限和角色类似,角色的前面加上ROLE_student 就变成了权限,并且是谁在后面谁起作用,无法都起作用

创建请求的三个控制器:

package com.powernode.controller;

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

@RestController
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a student,My name is Eric!";
    }
}
package com.powernode.controller;

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

@RestController
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a teacher,My name is Thomas!";
    }
}
package com.powernode.controller;

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

@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a administrator, My name is Obama!";
    }
}
package com.powernode.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 定义一个bean,用户详情服务接口
 */
@Configuration
public class MySecurityUserConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        // 创建了2个用户,系统的用户
        UserDetails user1 = User.builder()
                .username("eric")
                .password(passwordEncoder().encode("123456"))
                .roles("student")  // 角色的前面加上ROLE_student 就变成了权限
                .authorities("stduent:delete", "student:add") // 配置了权限
                .build();
        UserDetails user2 = User.builder()
                .username("thomas")
                .password(passwordEncoder().encode("123456"))
                .authorities("teacher:delete", "teacher:add") // 配置了权限
                .roles("teacher") // ROLE_teacher
                .build();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(user1);
        manager.createUser(user2);

        return manager;
    }

    /**
     * 自定义用户必须配置密码编码器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

新建MySecurityUserConfig 配置类

package com.powernode.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 定义一个bean,用户详情服务接口
 */
@Configuration
public class MySecurityUserConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        // 创建了2个用户,系统的用户
        UserDetails user1 = User.builder()
                .username("eric")
                .password(passwordEncoder().encode("123456"))
                .roles("student")  // 角色的前面加上ROLE_student 就变成了权限
                .authorities("stduent:delete", "student:add") // 配置了权限
                .build();
        UserDetails user2 = User.builder()
                .username("thomas")
                .password(passwordEncoder().encode("123456"))
                .authorities("teacher:delete", "teacher:add") // 配置了权限
                .roles("teacher") // ROLE_teacher
                .build();
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(user1);
        manager.createUser(user2);

        return manager;
    }

    /**
     * 自定义用户必须配置密码编码器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

编写查看用户信息的控制器:

package com.powernode.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@Slf4j
public class CurrentLoginUserController {
    // Authentication继承Principal,只要登录成功,就可以获取到登录的信息
    @GetMapping("/getLoginUser1")
    public Authentication getLoginUser1(Authentication authentication) {
        return authentication;
    }

    @GetMapping("/getLoginUser2")
    public Principal getLoginUser2(Principal principal) {
        return principal;
    }

    @GetMapping("/getLoginUser3")
    public Principal getLoginUser3() {
        // 通过安全上下文持有器获取安全上下文,再获取认证信息(返回的是Authentication接口)
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

通过请求查看权限:

查看之前要先登录认证成功,认证成功后,把信息放到Authentication类中,再把信息放到安全上下文(SecurityContext)中,才能通过上面代码中的三种方式获取UserDetails中对应用户的认证信息

通过http://localhost:8080/getLoginUser3(getLoginUser2、getLoginUser1)查看权限

查看权限

授权(对URL进行授权)

对URL进行授权也就是控制controller层,有两种方式:
1、就是在配置类中使用匹配路径url方式(mvcMatchers)控制
2、用注解的形式,在控制器中方法前加上,而没在控制器中对url控制,也能实现效果(一般不这么用)

此处这个程序的问题:无法动态创建用户

上面实现了认证功能,但是受保护的资源是默认的,默认所有认证(登录)用户均可以访问所有资源,不能根据实际情况进行角色管理
要实现授权功能,需重写WebSecurityConfigureAdapter 中的一个configure方法

1、引入依赖
2、创建请求的控制器
3、创建含有用户详细信息服务(UserDetailsService)的配置类(和上面的那个MySecurityUserConfig 一样)
4、创建一个新的配置类WebSecurityConfig,继承WebSecurityConfigurerAdapter,重写configure,配置权限能访问的请求
5、创建获取权限的控制器类
6、运行观察结果

创建请求的三个控制器:

package com.powernode.controller;

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

@RestController
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a student,My name is Eric!";
    }
}
package com.powernode.controller;

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

@RestController
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a teacher,My name is Thomas!";
    }
}
package com.powernode.controller;

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

@RestController
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("/query")
    public String queryInfo(){
        return "I am a administrator, My name is Obama!";
    }
}

配置类WebSecurityConfig:

package com.powernode.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 针对url进行授权
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 路径和需要的权限要对应,只有满足才能进行访问
        http.authorizeRequests() // 授权请求
                // 匹配路径url的三种方式:
//                .regexMatchers("/studnet/**") //不用学
//                .antMatchers("/student/**")  // 不用学
                .mvcMatchers("/student/**") // 匹配这个url
                // 权限方式五种:
                // .hasAuthority()  是否有单个权限
                // .access("hasAuthority('student:add') or hasRole('admin')") 通过el表达式书写条件
                // .hasRole() 是否有单个角色
                // .hasAnyRole() 是否有任意角色
                .hasAnyAuthority("student:add") // 拥有这个权限的用户可以访问的/student/**

                .mvcMatchers("/teacher/**") // 匹配url
                .hasAuthority("ROLE_teacher") // 拥有这个权限的用户可以访问的/teacher/**

                .anyRequest() // 任何请求
//                .denyAll()  // 拒绝
//                .permitAll(); // 允许任何请求
                .authenticated(); // 都需要登录, 注意:没有配置mvc的只要登录成功就可以访问
        // 这里增加配置了允许表单登录,就算上面全部拒绝,这里的配置让表单登录也能访问
        http.formLogin().permitAll(); // 允许表单登录 permit:允许
    }
}

创建获取权限的控制器类:

package com.powernode.controller;


import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
@Slf4j
public class CurrentLoginUserController {
    @GetMapping("/getLoginUser1")
    public Authentication getLoginUser1(Authentication authentication){
        return authentication;
    }

    @GetMapping("/getLoginUser2")
    public Principal getLoginUser2(Principal principal){
        return principal;
    }

    @GetMapping("/getLoginUser3")
    public Principal getLoginUser3(){
        //通过安全上下文持有器获取安全上下文,再获取认证信息
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

授权(方法级别的权限控制)

在web安全配置类上加上@EnableGlobalMethodSecurity注解后,再去方法上使用@PreAuthorize进行预授权或使用其他的注解进行权限控制

1、引入依赖
2、创建包和启动类
3、创建测试controller
4、创建获取当前用户信息的controller类(和前面的CurrentLoginUserController 一样)
5、创建两个配置类:
   一个是安全用户配置类中配置用户详细信息服务(和之前的MySecurityUserConfig一样)
   另一个是WebSecurityConfig继承WebSecurityConfigurerAdapter并重写里面的configure方法配置权限,并在这个配置类上添加@EnableGlobalMethodSecurity开启基于注解的安全配置
6、创建service并完成实现
7、启动程序测试

测试controller:

package com.powernode.controller;

import com.powernode.service.TeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/teacher")
@Slf4j
public class TeacherController {
    @Resource
    private TeacherService teacherService;

    @GetMapping("/query")
    public String queryInfo() {
        return teacherService.query();
    }

    @GetMapping("/add")
    public String addInfo() {
        return teacherService.add();
    }

    @GetMapping("/update")
    public String updateInfo() {
        int a = 10;
        log.info("进入更新教师controller");
        // 这里采用的是预授权, 所以执行方法前的代码仍会执行
        return teacherService.update();
    }

    @GetMapping("/delete")
    public String deleteInfo() {
        return teacherService.delete();
    }
}

WebSecurityConfig:

package com.powernode.config;

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

// TODO 1.对service(方法)控制
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启全局方法安全,启用预授权注解和后授权注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // TODO controller 的权限配置(mvcMethods),可以在这里配置(此处未配置)
        http.authorizeRequests().anyRequest().authenticated(); // 任何请求均需要认证

        http.formLogin().permitAll();  // 放开登录页面及登录接口
    }
}

service:

package com.powernode.service;

public interface TeacherService {
    String add();
    String update();
    String delete();
    String query();
}

service实现类:

package com.powernode.service.impl;

import com.powernode.service.TeacherService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class TeacherServiceImpl implements TeacherService {
    @Override
    // TODO 2.对方法进行权限控制
    // 预授权:访问方法前看有没有权限
    // 后授权:访问方法完成后再看
    @PreAuthorize("hasAuthority('teacher:add')") // 预授权
    public String add() {
        log.info("添加教师成功");
        return "添加教师成功";
    }

    @Override
    @PreAuthorize("hasAnyAuthority('teacher:update')") // 预授权
    public String update() {
        log.info("修改教师成功");
        return "修改教师成功";
    }

    @Override
    @PreAuthorize("hasAuthority('teacher:delete')") // 预授权
    public String delete() {
        log.info("删除教师成功");
        return "删除教师成功";
    }

    @Override
    @PreAuthorize("hasAnyAuthority('teacher:query')") // 预授权
    public String query() {
        log.info("查询教师成功");
        return "查询教师成功";
    }
}

SpringSecurity 返回json

1、引入依赖
2、创建请求的控制器(和前面的三个测试控制器一样)
3、创建VO(value object值对象),封装返回给前端的信息
4、创建获取当前用户信息的controller类(和前面的CurrentLoginUserController 一样)
5、创建含有用户详细信息服务(UserDetailsService)的配置类(和上面的那个MySecurityUserConfig一样)
6、创建处理器配置类
	认证成功处理器:实现接口AuthenticationSuccessHandler(认证成功处理器)、new这个接口实现里面的方法或采用λ表达式实现
	认证失败处理器
	退出成功处理器
	拒绝访问处理器
7、创建一个新的配置类WebSecurityConfig,继承WebSecurityConfigurerAdapter,重写configure,配置权限能访问的请求
8、运行观察结果

创建VO:

package com.powernode.vo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HttpResult {
    private Integer code; //返回给前端的自定义响应码
    private String msg;  // 返回给前端的消息
    private Object data; //返回给前端的数据
}

认证成功处理器:

package com.powernode.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powernode.vo.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 认证成功就会调用该接口里的方法
 */
@Component
@Slf4j
public class AppAutheticationSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    // 可以序列化为json,也可以把json转换成对象实现反序列化
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HttpResult httpResult = HttpResult.builder()
                .code(1)
                .msg("登录成功")
                .build();
        String responseJson = objectMapper.writeValueAsString(httpResult);

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }
}

认证失败处理器:

package com.powernode.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powernode.vo.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 认证失败就会调用该接口里的方法
 */
@Component
@Slf4j
public class AppAuthenticationFailHandler implements AuthenticationFailureHandler {
    @Resource
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        HttpResult httpResult=HttpResult.builder()
                .code(0)
                .msg("登录失败")
                .build();
        String responseJson = objectMapper.writeValueAsString(httpResult);

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }
}

退出成功处理器:

package com.powernode.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powernode.vo.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 退出成功成功就会调用该接口里的方法
 */
@Component
@Slf4j
public class AppLogoutSuccessHanlder implements LogoutSuccessHandler {
    @Resource
    private ObjectMapper objectMapper;
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        HttpResult httpResult=HttpResult.builder()
                .code(1)
                .msg("退出成功")
                .build();
        String responseJson = objectMapper.writeValueAsString(httpResult);

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }
}

拒绝访问处理器:

package com.powernode.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.powernode.vo.HttpResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 没有权限就会调用该接口里的方法
 */
@Component
@Slf4j
public class AppAccessDenyHanlder implements AccessDeniedHandler {
    @Resource
    private ObjectMapper objectMapper;
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        HttpResult httpResult=HttpResult.builder()
                .code(0)
                .msg("您没有权限访问该资源!!")
                .build();
        String responseJson = objectMapper.writeValueAsString(httpResult);

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(responseJson);
        writer.flush();
    }
}

新的配置类WebSecurityConfig:

package com.powernode.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private AppAutheticationSuccessHandler appAutheticationSuccessHandler;
    @Autowired
    private AppAuthenticationFailHandler appAuthenticationFailHandler;

    @Autowired
    private AppLogoutSuccessHanlder appLogoutSuccessHanlder;
    @Autowired
    private AppAccessDenyHanlder appAccessDenyHanlder;


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/student/**") // 配置访问路径
                .hasRole("student") // 配置访问这个路径需要的角色
                .anyRequest().authenticated();
        // 设置登录成功的处理器
        http.formLogin()
                .successHandler(appAutheticationSuccessHandler) // 配置认证成功处理器
                .failureHandler(appAuthenticationFailHandler) // 配置认证失败处理器
                .permitAll();
        http.logout().logoutSuccessHandler(appLogoutSuccessHanlder);// 配置退出成功处理器
        // 配置访问拒绝处理器
        http.exceptionHandling().accessDeniedHandler(appAccessDenyHanlder);
    }
}

使用UserDetailsService获取用户认证信息

1、引入依赖
2、创建请求的控制器(和前面的三个测试控制器一样)
3、创建VO(value object值对象),封装返回给前端的信息(和前面的HttpResult一样)
4、创建一个类实现用户详细信息(UserDetails)接口,里面配置用户相关的信息
4、创建获取当前用户信息的controller类(和前面的CurrentLoginUserController 一样)
5、创建一个实现用户详细信息服务(UserDetailsService)接口的服务(和MySecurityUserConfig采用Bean注入的方式不一样)
6、创建处理器配置类(以下的配置类,和上面的那四个一样)
	认证成功处理器:实现接口AuthenticationSuccessHandler(认证成功处理器)、new这个接口实现里面的方法或采用λ表达式实现
	认证失败处理器
	退出成功处理器
	拒绝访问处理器
7、创建一个新的配置类WebSecurityConfig,继承WebSecurityConfigurerAdapter,重写configure,配置权限能访问的请求。以及配置密码编码器。
8、运行观察结果

用户详细信息(UserDetails)接口方法说明:

方法说明

创建一个类实现用户详细信息(UserDetails)接口:

package com.powernode.vo;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import java.util.Collection;

public class SecurityUser implements UserDetails {
    // 用户的权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return new BCryptPasswordEncoder().encode("123456");
    }

    @Override
    public String getUsername() {
        return "thomas";
    }

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

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

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

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

创建一个实现用户详细信息服务(UserDetailsService)接口的服务:

package com.powernode.service.impl;

import com.powernode.vo.SecurityUser;
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 org.springframework.util.StringUtils;

@Service
public class UserServiceImpl implements UserDetailsService {
    /**
     * 根据用户名获取用户的详情
     *
     * @param username
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (!StringUtils.hasText(username)) { // 用户名为空
            throw new UsernameNotFoundException("用户名不存在!");
        }
        if (!username.equals("thomas")) {
            throw new UsernameNotFoundException("用户名不正确!");
        }
        // 执行到这里,说明用户名username =thomas
        return new SecurityUser();
    }
}

创建一个新的配置类WebSecurityConfig:

package com.powernode.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;

@Configuration
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private AppAutheticationSuccessHandler appAutheticationSuccessHandler;
    @Resource
    private AppAuthenticationFailHandler appAuthenticationFailHandler;
    @Resource
    private AppLogoutSuccessHanlder appLogoutSuccessHanlder;
    @Resource
    private AppAccessDenyHanlder appAccessDenyHanlder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .mvcMatchers("/student/**")
                .hasRole("student")
                .anyRequest().authenticated();
        // 设置登录成功的处理器
        http.formLogin()
                .successHandler(appAutheticationSuccessHandler) // 配置认证成功处理器
                .failureHandler(appAuthenticationFailHandler) // 配置认证失败处理器
                .permitAll();
        http.logout().logoutSuccessHandler(appLogoutSuccessHanlder);// 配置退出成功处理器
        // 配置访问拒绝处理器
        http.exceptionHandling().accessDeniedHandler(appAccessDenyHanlder);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

基于数据库的认证和方法授权

1、创建数据库
2、引入SpringSecurity、Mybatis、Mysql依赖
3、配置数据源和mybatis
4、配置实体类
5、配置mapper和映射文件
6、新建启动类
7、单元测试
8、新建安全用户类
9、新建service,根据用户名获取用户信息
10、新建serviceImpl实现service接口
11、新建SecurityUserDetailsServiceImpl实现UserDetailService接口
12、新建service单元测试
13、新建两个控制器
14、新建获取登录用户认证信息的controller(和前面的CurrentLoginUserController一样)
15、新建web安全配置类

配置数据源和mybatis:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver # mysql8依赖
    url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: root
    password: 1234

mybatis:
  mapper-locations: classpath:mapper/*.xml # 映射文件存放位置(resources/mapper/)
  type-aliases-package: com.powernode.entity # 别名扫描包, 在映射文件中使用不需要写全称了(resultType="类名改首字母为小写"), 直接写类名把首字母小写就可以了
  configuration:
    map-underscore-to-camel-case: true # 驼峰命名
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志实现, 直接在控制台输出

配置实体类:

package com.powernode.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysUser implements Serializable {
    private Integer userId;
    private String username;
    private String password;
    private String sex;
    private String address;
    private Integer enabled;
    private Integer accountNoExpired;
    private Integer credentialsNoExpired;
    private Integer accountNoLocked;
}
package com.powernode.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SysMenu implements Serializable {
    private Integer id;
    private Integer pid;
    private Integer type;
    private String name;
    private String code;
}

配置mapper:

package com.powernode.dao;

import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;

public interface SysUserDao {
    /**
     * 根据用户名获取用户信息
     *
     * @param name
     * @return
     */
    // @Param("userName"): 和参数对应, 形参可以随便命名, 但是注解中的名字是实体类中的属性名,
    //      在xml文件中会使用到这个实体类中的属性, 如果不写注解, 就需要两个的名字相同
    SysUser getByUserName(@Param("userName") String name);
}
package com.powernode.dao;

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface SysMenuDao {
    List<String> queryPermissionsByUserId(@Param("userId") Integer userId);
}

配置映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysUserDao">
    <select id="getByUserName" resultType="sysUser">
        select user_id,
               username,
               password,
               sex,
               address,
               enabled,
               account_no_expired,
               credentials_no_expired,
               account_no_locked
        from sys_user
        where username = #{userName}
    </select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.powernode.dao.SysMenuDao">
    <select id="queryPermissionsByUserId" resultType="string">
        select distinct sm.code
        from sys_role_user sru
                 inner join sys_role_menu srm on sru.rid = srm.rid
                 inner join sys_menu sm on srm.mid = sm.id
        where sru.uid = #{userId}
    </select>
</mapper>

测试dao:

package com.powernode.dao;

import com.powernode.entity.SysUser;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SysUserDaoTest {

    @Resource
    private SysUserDao sysUserDao;

    @Test
    void getByUserName() {
        SysUser sysUser = sysUserDao.getByUserName("obama");
        assertNotNull(sysUser);
    }
}
package com.powernode.dao;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SysMenuDaoTest {
    @Resource
    private SysMenuDao sysMenuDao;
    @Test
    void queryPermissionsByUserId() {
        List<String> resultList = sysMenuDao.queryPermissionsByUserId(1);
        assertTrue(!resultList.isEmpty()); //判断list不为空
    }
}

新建安全用户类:

package com.powernode.vo;

import com.powernode.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class MySecurityUser implements UserDetails {
    private final SysUser sysUser;
    //用于存储权限的list
    private List<SimpleGrantedAuthority> authorityList;
    public MySecurityUser(SysUser sysUser){
        this.sysUser=sysUser;
    }

    /**
     * 返回用户所拥有的权限
     * @return
     */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorityList;
    }

    public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
        this.authorityList = authorityList;
    }

    @Override
    public String getPassword() {
        String myPassword=sysUser.getPassword();
        sysUser.setPassword(null); //擦除我们的密码,防止传到前端
        return myPassword;
    }

    @Override
    public String getUsername() {
        return this.sysUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return sysUser.getAccountNoExpired().equals(1);
    }

    @Override
    public boolean isAccountNonLocked() {
        return sysUser.getAccountNoLocked().equals(1);
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return sysUser.getCredentialsNoExpired().equals(1);
    }

    @Override
    public boolean isEnabled() {
        return sysUser.getEnabled().equals(1);
    }
}

新建service,根据用户名获取用户信息:

package com.powernode.service;

import com.powernode.entity.SysUser;
import org.apache.ibatis.annotations.Param;

public interface SysUserService {
    /**
     * 根据用户名获取用户信息
     * @param name
     * @return
     */
    SysUser getByUserName(String name);
}
package com.powernode.service;

import org.apache.ibatis.annotations.Param;

import java.util.List;

public interface SysMenuService {
    List<String> queryPermissionsByUserId(Integer userId);
}

新建serviceImpl实现service接口:

package com.powernode.service.impl;

import com.powernode.dao.SysUserDao;
import com.powernode.entity.SysUser;
import com.powernode.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class SysUserServiceImpl implements SysUserService {
    @Resource
    private SysUserDao sysUserDao;
    @Override
    public SysUser getByUserName(String userName) {
        return sysUserDao.getByUserName(userName);
    }
}
package com.powernode.service.impl;

import com.powernode.dao.SysMenuDao;
import com.powernode.service.SysMenuService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
@Slf4j
public class SysMenuServiceImpl implements SysMenuService {
    @Resource
    private SysMenuDao sysMenuDao;
    @Override
    public List<String> queryPermissionsByUserId(Integer userId) {
        return sysMenuDao.queryPermissionsByUserId(userId);
    }
}

新建service单元测试:

package com.powernode.service.impl;

import com.powernode.entity.SysUser;
import com.powernode.service.SysUserService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class SysUserServiceImplTest {

    @Resource
    private SysUserService sysUserService;

    @Test
    void getByUserName() {
        SysUser sysUser = sysUserService.getByUserName("dddd");
        assertNull(sysUser);
    }
}

新建SecurityUserDetailsServiceImpl 实现UserDetailService接口:

package com.powernode.service.impl;

import com.powernode.entity.SysUser;
import com.powernode.service.SysMenuService;
import com.powernode.service.SysUserService;
import com.powernode.vo.MySecurityUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SecurityUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private SysUserService sysUserService;
    @Resource
    private SysMenuService sysMenuService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getByUserName(username);
        if (null == sysUser) {
            throw new UsernameNotFoundException("该用户不存在");
        }
        // 根据用户id获取该用户所拥有的权限,List<SimpleGrantedAuthority>
        List<String> userPermissions = sysMenuService.queryPermissionsByUserId(sysUser.getUserId());

        // 遍历权限,把权限放到列表中
        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        for (String userPermission : userPermissions) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
            authorityList.add(simpleGrantedAuthority);
        }

        List<SimpleGrantedAuthority> authorityList1 = userPermissions.stream()
                /*.map(userPermission -> {
                    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(userPermission);
                    return simpleGrantedAuthority;
                })*/
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());


        MySecurityUser securityUser = new MySecurityUser(sysUser);
        securityUser.setAuthorityList(authorityList);
        return securityUser;
    }
}

新建两个控制器:

package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/student")
public class StudentController {
    @GetMapping("/query")
    public String queryInfo(){
        return "query student";
    }
    @GetMapping("/add")
    public String addInfo(){
        return "add  student!";
    }
    @GetMapping("/update")
    public String updateInfo(){
        return "update student";
    }
    @GetMapping("/delete")
    public String deleteInfo(){
        return "delete  student!";
    }
    @GetMapping("/export")
    public String exportInfo(){
        return "export  student!";
    }
}
package com.powernode.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/teacher")
public class TeacherController {
    @GetMapping("/query")
    @PreAuthorize("hasAuthority('teacher:query')") // 预授权
    public String queryInfo() {
        return "I am a teacher!";
    }
}

新建web安全配置类:

package com.powernode.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@EnableGlobalMethodSecurity(prePostEnabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().permitAll();
        http.authorizeRequests().anyRequest().authenticated();
    }
}

base64编码

选出64个字符:小写字母a-z、大写字母A-Z、数字0-9、符号"+"、"/"(再加上作为垫字的"=",等号只能出现在最后,实际上是使用65个字符),作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。
不管文件多大,从左到右,每三个字节变成四个字节,这四个字节就是base64里的字符了。
余数为0不会看见等号,余数为1后面加两个等号,余数为2后面加一个等号。

echo 命令是带换行符的
echo -n 不换行输出

用base64命令编码字符串:

编码去回车

用base64命令解码字符串:

解码

base64 编解码文件:

编码
base64 待编码的文件名 > 编码后的文件名
base64 1.mp3 > mymp3
解码
base64 -d 待解码的文件名 >解码后的文件名
base64 -d mymp3>88.mp3

Base64Url(在Base64的基础上编码形成新的编码方式)

Base64Url 编码的流程:

1、明文使用BASE64进行编码
2、在Base64编码的基础上进行以下的处理:
    1)去除尾部的"="
    2)把"+"替换成"-"
    3)斜线"/"替换成下划线"_"

跨域认证问题

一般流程:

用户向服务器发送用户名和密码。
服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
服务器向用户返回一个 jsession_id,写入用户的 Cookie。
用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。如何实现呢,有两个办法:

一种解决方案是 session 数据持久化,写入数据库或别的持久层。这种方案优点是架构清晰,缺点是工程量比较大。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT 实现登录原理图

jwt原理图

说明:

JWT只通过算法实现对Token合法性的验证,不依赖数据库Memcached的等存储系统,因此可以做到跨服务器验证,只要密钥和算法相同,不同服务器程序生成的Token可以互相验证。

JWT学习

JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。JWT就是一个加密的带用户信息的字符串。
 此信息可以通过数字签名进行验证和信任。 
 JWT可以使用密钥(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。

JWT组成:(三部分组成,各部分以点分隔)

Header(头部)-----base64Url编码的Json字符串
Payload(载荷)---base64url编码的Json字符串
Signature(签名)---使用指定算法,通过Header和Playload加盐计算的字符串

Header(头部):

一部分是token的类型,目前只能是JWT
另一部分是签名算法,比如HMAC 、 SHA256 、 RSA

base64编码命令:echo -n '{"alg":"HS256","typ":"JWT"}' | base64

Payload(载荷):

包含claims(声明)。
Claims是关于一个实体(通常是用户)和其他数据类型的声明。
claims有三种类型:registered,public,and private claims。

Registered(已注册的声明):这些是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。 
其中一些是:iss(发行人),exp(到期时间),sub(主题),aud(观众)and others。(请注意,声明名称只有三个字符,因为JWT意味着紧凑。)
JWT 规定了7个官方字段,供选用:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息(密码,手机号等)放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Public(公开声明):这些可以由使用JWT的人随意定义。 
但为避免冲突,应在IANA JSON Web Token Registry中定义它们,或者将其定义为包含防冲突命名空间的URI。

private (私人声明):这些声明是为了在同意使用它们的各方之间共享信息而创建的,并且既不是注册声明也不是公开声明。

Signature(签名,保证数据安全性的):

Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名:
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

JWT 的使用方式【重点】:

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面
Authorization: Bearer jwt
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面

JWT 的几个特点:

JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
JWT 不加密的情况下,不能将秘密数据写入 JWT。
JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑(JWT的登出问题)。又因为服务端不保留token ,所以我们如果在jwt过期前修改密码,退出了系统,但是继续用之前的token 登录,还是可以继续访问的,这样肯定是不行的,于是要结合redis一起完成退出功能

JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。
为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
为了减少盗用,JWT 不应该使用 HTTP 80 协议明码传输,要使用 HTTPS 443 协议传输。

java中使用jwt

添加依赖
编写功能类
写主类测试一下

添加依赖:

<dependencies>
	<!-- 添加jwt的依赖 -->
	<dependency>
		<groupId>com.auth0</groupId>
		<artifactId>java-jwt</artifactId>
		<version>3.11.0</version>
	</dependency>
</dependencies>

编写jwt功能类:

package com.powernode.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class JwtUtils {
    private static final String secret = "secret888"; // 密钥

    /**
     * 创建jwt令牌
     *
     * @param userId
     * @param userName
     * @param authList 权限
     * @return
     */
    public String createJwt(Integer userId, String userName, List<String> authList) {
        Date issDate = new Date(); // 签发时间时间
//        Date expireDate = new Date(issDate.getTime() + 1000 *30); //过期时间30秒
        Date expireDate = new Date(issDate.getTime() + 1000 * 60 * 60 * 2); // 当前时间加上两个小时

        // 头部
        Map<String, Object> headerClaims = new HashMap<>();
        headerClaims.put("alg", "HS256");
        headerClaims.put("typ", "JWT");
        return JWT.create()
                .withHeader(headerClaims) // 头部
                .withIssuer("thomas") // 设置签发人
                .withIssuedAt(issDate) // 签发时间
                .withExpiresAt(expireDate) // 过期时间
                .withClaim("userId", userId) // 自定义声明
                .withClaim("userName", userName)// 自定义声明
                .withClaim("userAuth", authList)// 自定义声明
                .sign(Algorithm.HMAC256(secret)); // 使用HS256进行签名,使用secret作为密钥
    }

    /**
     * 校验token
     * @param jwtToken
     * @return
     */
    public boolean verifyToken(String jwtToken) {
        // 创建校验器
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
            // 校验token
            DecodedJWT decodedJwt = jwtVerifier.verify(jwtToken);
            System.out.println("token验证正确");
//            Integer userId = decodedJwt.getClaim("userId").asInt();
//            String userName = decodedJwt.getClaim("userName").asString();
//            List<String> userAuth = decodedJwt.getClaim("userAuth").asList(String.class);
            return true;
        } catch (Exception e) {
            System.out.println("token验证不正确!!!");
            return false;
        }
    }

    /**
     * 从jwt的payload里获取声明,获取的用户id
     *
     * @param jwt
     * @return
     */
    public Integer getUserIdFromToken(String jwt) {
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
            return decodedJWT.getClaim("userId").asInt();
        } catch (IllegalArgumentException e) {
            return -1;
        } catch (JWTVerificationException e) {
            return -1;
        }
    }

    /**
     * 从jwt的payload里获取声明,获取的用户名
     *
     * @param jwt
     * @return
     */
    public String getUserNameFromToken(String jwt) {
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
            return decodedJWT.getClaim("userName").asString();
        } catch (IllegalArgumentException e) {
            return "";
        } catch (JWTVerificationException e) {
            return "";
        }
    }

    /**
     * 从jwt的payload里获取声明,获取的用户的权限
     *
     * @param jwt
     * @return
     */
    public List<String> getUserAuthFromToken(String jwt) {
        try {
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(jwt);
            return decodedJWT.getClaim("userAuth").asList(String.class);
        } catch (IllegalArgumentException | JWTVerificationException e) {
            return null;
        }
    }
}

jwt测试类:

package com.powernode.util;

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

public class JwtTest {
    public static void main(String[] args) {
        JwtTest jwtTest = new JwtTest();
        String token = jwtTest.createToken();
        JwtUtils jwtUtils = new JwtUtils();
        boolean verifyResult = jwtUtils.verifyToken(token);
        if (verifyResult) {
            // 从token获取权限
            List<String> authList = jwtUtils.getUserAuthFromToken(token);
            System.out.println(authList);
        }
//       jwtTest.verifyToken();
    }

    /**
     * 创建token
     *
     * @return
     */
    public String createToken() {
        JwtUtils jwtUtils = new JwtUtils();
        List<String> authList = Arrays.asList("student:query", "student:add", "student:update");
        String myCreateJwt = jwtUtils.createJwt(19, "obama", authList);
        System.out.println(myCreateJwt);
        return myCreateJwt;
    }

    // 简单测试
    public void verifyToken() {
        String jwt = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyQXV0aCI6WyJzdHVkZW50OnF1ZXJ5Iiwic3R1ZGVudDphZGQiLCJzdHVkZW50OnVwZGF0ZSJdLCJpc3MiOiJ0aG9tYXMiLCJleHAiOjE2NzU3MzMwNzcsInVzZXJOYW1lIjoib2JhbWEiLCJpYXQiOjE2NzU3MzMwNDcsInVzZXJJZCI6MTl9.ZdRnI5XLFaw6mc1J7X5beHp0KsiYBZczanYzrnvT8yQ";
        JwtUtils jwtUtils = new JwtUtils();
        boolean verifyResult = jwtUtils.verifyToken(jwt);
        System.out.println(verifyResult);
    }
}

结果:

测试结果

代码开发注意事项:

开发代码时不允许使用main方法测试,而是使用单元测试来测试
代码中一般不允许使用System.out.println 直接输出,而是使用日志输出
单元测试尽量使用断言,而不是使用System.out.println输出

编码顺序:entity--》dao--》dao的单元测试--》service接口---》service实现--》service的单元测试--》controller--》controller的单元测试

对比结果发现,相同的字符串加密之后的结果都不一样,但是比较的时候是一样的,因为加了盐(salt)了。
输出结果不同

扩展

通过curl访问程序(可以在git bash中使用)【curl是常用的命令行工具,用来请求Web服务器。
发请求,接收响应,接收的响应只能是html或是json,并不能显示】
默认访问方式:
curl默认访问网址格式

发送指定请求方式:
curl发送post请求
查看整个请求和响应的过程:
curl查看整个请求和响应的过程
带用户名和密码访问请求:
通过用户名和密码访问

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值