基于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入门使用
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只通过算法实现对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,并不能显示】
默认访问方式:
发送指定请求方式:
查看整个请求和响应的过程:
带用户名和密码访问请求: