SpringSecurity Web 应用的安全性包括用户认证 (Authentication)和用户授权 (Authorization)两个部分,这两点也是Spring Security重要核心功能。
因此,一般来说,常见的安全管理技术栈的组合是这样的:
SSM + Shiro
•Spring Boot/Spring Cloud + Spring Security
以上只是一个推荐的组合而已,如果单纯从技术上来说,无论怎么组合,都是可以运行 的。
入门案例
添加一个配置类
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .and() .authorizeRequests() .anyRequest() .authenticated(); } }
运行项目访问8080
默认的用户名:user 密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都回发生变化!
输入用户名,密码,这样表示可以访问了,404 表示我们没有这个控制器,但是我们可以 访问了。
权限管理中的相关概念 主体 英文单词:principal 使用系统的用户或设备或从其他系统远程登录的用户等等。简单说就是谁使用系 统谁就是主体。
认证 英文单词:authentication 权限管理系统确认一个主体的身份,允许主体进入系统。简单说就是“主体”证 明自己是谁。 笼统的认为就是以前所做的登录操作。
授权 英文单词:authorization 将操作系统的“权力”“授予”“主体”,这样主体就具备了操作系统中特定功 能的能力。 所以简单来说,授权就是给用户分配权限。
完善案例 添加一个控制器进行访问
1 2 3 4 5 6 7 8 @Controller public class IndexController { @GetMapping("index") @ResponseBody public String index () { return "success" ; } }
SpringSecurity基本原理 SpringSecurity 本质是一个过滤器链:
从启动是可以获取到过滤器链:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFil ter org.springframework.security.web.context.SecurityContextPersistenceFilter org.springframework.security.web.header.HeaderWriterFilter org.springframework.security.web.csrf.CsrfFilter org.springframework.security.web.authentication.logout.LogoutFilter org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter org.springframework.security.web.savedrequest.RequestCacheAwareFilter org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter org.springframework.security.web.authentication.AnonymousAuthenticationFilter org.springframework.security.web.session.SessionManagementFilter org.springframework.security.web.access.ExceptionTranslationFilter org.springframework.security.web.access.intercept.FilterSecurityInterceptor
代码底层流程:重点看三个过滤器:
FilterSecurityInterceptor :是一个方法级的权限过滤器, 基本位于过滤链的最底部。
super.beforeInvocation(fi)表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务。
ExceptionTranslationFilter :是个异常过滤器,用来处理在认证授权过程中抛出的异常
UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户 名,密码。
当过滤器过滤的过程中会走到对应的接口里面执行方法。
UserDetailService 接口讲解 当什么也没有配置的时候,账号和密码是由Spring Security定义生成的。而在实际项目中 账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。 如果需要自定义逻辑时,只需要实现UserDetailsService接口即可。接口定义如下:
返回值UserDetails 这个类是系统默认的用户“主体”
在这个接口里面有很多方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Collection<? extends GrantedAuthority > getAuthorities(); String getPassword () ; String getUsername () ; boolean isAccountNonExpired () ; boolean isAccountNonLocked () ; boolean isCredentialsNonExpired () ; boolean isEnabled () ;
以下是UserDetails实现类
以后我们只需要使用User 这个实体类即可!
方法参数 username 表示用户名。此值是客户端表单传递过来的数据。默认情况下必须叫username,否则无 法接收。
PasswordEncoder 接口讲解 1 2 3 4 5 6 7 8 String encode (CharSequence rawPassword) ; boolean matches (CharSequence rawPassword, String encodedPassword) ; default boolean upgradeEncoding (String encodedPassword) { return false ; }
接口实现类
BCryptPasswordEncoder是Spring Security官方推荐的密码解析器,平时多使用这个解析 器。 BCryptPasswordEncoder是对bcrypt强散列方法的具体实现。是基于Hash算法实现的单 向加密。可以通过strength控制加密强度,默认10.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test01 () { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder (); String atguigu = bCryptPasswordEncoder.encode("atguigu" ); System.out.println("加密之后数据:\t" +atguigu); boolean result = bCryptPasswordEncoder.matches("atguigu" , atguigu); System.out.println("比较结果:\t" +result); }
当然这些都是单独使用springsecurity的方式,如果用SpringBoot他会对Security进行自动配置,可以在yml或者自定义类里面修改配置即可。
SpringSecurity Web权限方案 设置登录系统的账号、密码 方式一:在application.properties
1 2 spring.security.user.name =atguigu spring.security.user.password =atguigu
方式二:编写类实现接口
1 2 3 4 5 6 7 8 @Configuration public class SecurityConfig { @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } }
然后编写登录类实现接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public class LoginService implements UserDetailsService { @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { if (!"admin" .equals(username)){ throw new UsernameNotFoundException ("用户名不存在!" ); } String pwd = "$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na" ; return new User (username,pwd,AuthorityUtils.commaSeparatedStringToAuthorityList("admin," )); } }
实现数据库认证来完成用户登录 完成自定义登录
准备sql 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 create table users( id bigint primary key auto_increment, username varchar (20 ) unique not null , password varchar (100 ) ); insert into users values (1 ,'张 san' ,'$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na' ); insert into users values (2 ,'李 si' ,'$2a$10$2R/M6iU3mCZt3ByG7kwYTeeW0w7/UqdeXrb27zkBIizBvAven0/na' ); create table role( id bigint primary key auto_increment, name varchar (20 ) ); insert into role values (1 ,'管理员' ); insert into role values (2 ,'普通用户' ); create table role_user( uid bigint , rid bigint ); insert into role_user values (1 ,1 ); insert into role_user values (2 ,2 ); create table menu( id bigint primary key auto_increment, name varchar (20 ), url varchar (100 ), parentid bigint , permission varchar (20 ) ); insert into menu values (1 ,'系统管理' ,'' ,0 ,'menu:system' ); insert into menu values (2 ,'用户管理' ,'' ,0 ,'menu:user' ); create table role_menu( mid bigint , rid bigint ); insert into role_menu values (1 ,1 ); insert into role_menu values (2 ,1 ); insert into role_menu values (2 ,2 );
添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.0.5</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > </dependencies >
制作实体类 1 2 3 4 5 6 @Data public class Users { private Integer id; private String username; private String password; }
整合MybatisPlus 制作mapper 1 2 3 @Repository public interface UsersMapper extends BaseMapper <Users> { }
1 2 3 4 5 6 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =root
制作登录实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service("userDetailsService") public class MyUserDetailsService implements UserDetailsService { @Autowired private UsersMapper usersMapper; @Override public UserDetails loadUserByUsername (String s) throws UsernameNotFoundException { QueryWrapper<Users> wrapper = new QueryWrapper (); wrapper.eq("username" ,s); Users users = usersMapper.selectOne(wrapper); if (users == null ) { throw new UsernameNotFoundException ("用户名不存在!" ); } System.out.println(users); List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role" ); return new User (users.getUsername(), new BCryptPasswordEncoder ().encode(users.getPassword()),auths); } }
未认证请求跳转到登录页 这里是让没有登录的用户跳转到登录页而不能访问服务器页面
引入前端模板依赖 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-thymeleaf</artifactId > </dependency >
引入登录页面 将准备好的登录页面导入项目中
编写控制器 1 2 3 4 5 6 7 8 9 10 11 12 @Controller public class IndexController { @GetMapping("index") public String index () { return "login" ; } @GetMapping("findAll") @ResponseBody public String findAll () { return "findAll" ; } }
编写配置类放行登录页面以及静态资源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder () { return new BCryptPasswordEncoder (); } @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/layui/**" ,"/index" ) .permitAll() .anyRequest() .authenticated(); } }
设置未授权的请求跳转到登录页 这个是登陆后没有权限的回到登录页
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override protected void configure (HttpSecurity http) throws Exception { http.formLogin() .loginPage("/index" ) .loginProcessingUrl("/login" ) .successForwardUrl("/success" ) .failureForwardUrl("/fail" ); http.authorizeRequests() .antMatchers("/layui/**" ,"/index" ) .permitAll() .anyRequest() .authenticated(); http.csrf().disable(); }
控制器
1 2 3 4 5 6 7 8 9 @PostMapping("/success") public String success () { return "success" ; } @PostMapping("/fail") public String fail () { return "fail" ; }
1 2 3 4 5 <form action ="/login" method ="post" > 用户名:<input type ="text" name ="username" /> <br /> 密码:<input type ="password" name ="password" /> <br /> <input type ="submit" value ="提交" /> </form >
注意:页面提交方式必须为post 请求,所以上面的页面不能使用,用户名,密码必须为 username,password 原因: 在执行登录的时候会走一个过滤器UsernamePasswordAuthenticationFilter
如果修改配置可以调用usernameParameter()和passwordParameter()方法。
1 2 3 4 5 <form action ="/login" method ="post" > 用户名:<input type ="text" name ="loginAcct" /> <br /> 密码:<input type ="password" name ="userPswd" /> <br /> <input type ="submit" value ="提交" /> </form >
基于角色或者权限进行访问控制 hasAuthority方法 如果当前的主体具有指定的权限,则返回 true,否则返回false
修改配置类
添加一个控制器
1 2 3 4 5 @GetMapping("/find") @ResponseBody public String find () { return "find" ; }
给用户登录主体赋予权限
测试: http://localhost:8090/findAll 访问findAll 进入登录页面
认证完成之后返回登录成功
hasAnyAuthority 方法 如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.
访问 http://localhost:8090/find
hasRole 方法 如果用户具备给定角色就允许访问,否则出现403。
如果当前主体具有指定的角色,则返回true。
底层源码:
给用户添加角色:
修改配置文件: 注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。
关闭csrf 是因为纯API服务,前后端分离,无Cookie自动携带,用Token认证如JWT
hasAnyRole 表示用户具备任何一个条件都可以访问。 给用户添加角色:
修改配置文件:
基于数据库实现权限认证 添加实体类 1 2 3 4 5 6 7 8 @Data public class Menu { private Long id; private String name; private String url; private Long parentId; private String permission; }
1 2 3 4 5 @Data public class Role { private Long id; private String name; }
编写接口与实现类 UserInfoMapper
1 2 3 4 5 6 7 8 9 10 11 12 13 List<Role> selectRoleByUserId (Long userId) ; List<Menu> selectMenuByUserId (Long userId) ;
上述接口需要进行多表管理查询: 需要在resource/mapper目录下自定义UserInfoMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?xml version="1.0" encoding="utf-8" ?> <!DOCTYPEmapperPUBLIC"-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.atguigu.mapper.UserInfoMapper" > <select id ="selectRoleByUserId" resultType ="com.atguigu.bean.Role" > SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON ru.rid=r.id where ru.uid=#{0} </select > <select id ="selectMenuByUserId" resultType ="com.atguigu.bean.Menu" > SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m INNER JOIN role_menu rm ON m.id=rm.mid INNER JOIN role r ON r.id=rm.rid INNER JOIN role_user ru ON r.id=ru.rid WHERE ru.uid=#{0} </select > </mapper >
UsersServiceImpl
在配置文件中添加映射 在配置文件中application.yml添加
1 2 mybatis: mapper-locations: classpath:mapper/*.xml
修改访问配置类
自定义403页面 修改访问配置类 1 http.exceptionHandling().accessDeniedPage("/unauth" );
添加对应控制器 1 2 3 4 @GetMapping("/unauth") public String accessDenyPage () { return "unauth" ; }
unauth.html
1 2 3 <body > <h1 > 对不起,您没有访问权限!</h1 > </body >
注解使用 @Secured 判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
使用注解先要开启注解功能!
@EnableGlobalMethodSecurity(securedEnabled=true)
1 2 3 4 5 6 7 @SpringBootApplication @EnableGlobalMethodSecurity(securedEnabled=true) public class DemosecurityApplication { public static void main (String[] args) { SpringApplication.run(DemosecurityApplication.class, args); } }
在控制器方法上添加注解
1 2 3 4 5 @RequestMapping("testSecured") @ResponseBody @Secured({"ROLE_normal","ROLE_admin"}) public String helloUser () { return "hello,user" ; }
登录之后直接访问:
http://localhost:8090/testSecured
将上述的角色改为 @Secured({“ROLEnormal”,”ROLE 管理员”})即可访问
@PreAuthorize 先开启注解功能: @EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用 户的roles/permissions参数传到方法中。
1 2 3 4 5 6 7 8 @RequestMapping("/preAuthorize") @ResponseBody @PreAuthorize("hasAnyAuthority('menu:system')") public String preAuthorize () { System.out.println("preAuthorize" ); return "preAuthorize" ; }
@PostAuthorize 先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值 的权限.
1 2 3 4 5 6 7 8 @RequestMapping("/testPostAuthorize") @ResponseBody @PostAuthorize("hasAnyAuthority('menu:system')") public String preAuthorize () { System.out.println("test--PostAuthorize" ); return "PostAuthorize" ; }
@PostFilter @PostFilter :权限验证之后对数据进行过滤 留下用户名是admin1的数据
表达式中的 filterObject 引用的是方法返回值List中的某一个元素
1 2 3 4 5 6 7 8 9 10 @RequestMapping("getAll") @PreAuthorize("hasRole('ROLE_管理员')") @PostFilter("filterObject.username == 'admin1'") @ResponseBody public List<UserInfo> getAllUser () { ArrayList<UserInfo> list = new ArrayList <>(); list.add(new UserInfo (1l ,"admin1" ,"6666" )); list.add(new UserInfo (2l ,"admin2" ,"888" )); return list; }
@PreFilter @PreFilter: 进入控制器之前对数据进行过滤
1 2 3 4 5 6 7 8 9 10 @RequestMapping("getTestPreFilter") @PreAuthorize("hasRole('ROLE_管理员')") @PreFilter(value = "filterObject.id%2==0") @ResponseBody public List<UserInfo> getTestPreFilter (@RequestBody List<UserInfo> list) { list.forEach(t-> { System.out.println(t.getId()+"\t" +t.getUsername()); }); return list; }
还有很多权限表达式,都是内置的具体要用的时候查找就行
表达式
说明
示例
hasRole('ROLE_ADMIN')
用户是否有角色(自动加 ROLE_ 前缀)
.access("hasRole('ADMIN')")
hasAnyRole('ADMIN','USER')
拥有任意一个角色
hasAuthority('user:delete')
用户是否有指定权限(Authority)
.access("hasAuthority('menu:system')")
hasAnyAuthority('a','b')
拥有任意一个权限
permitAll
允许所有访问
.access("permitAll")
denyAll
拒绝所有访问
isAuthenticated()
用户已认证(非匿名)
isAnonymous()
用户是匿名的
isRememberMe()
通过 Remember-Me 登录
principal
当前用户主体(UserDetails 对象)
principal.username == 'admin'
authentication
当前认证对象
authentication.principal.username == '...'
基于数据库的记住我 创建表 1 2 3 4 5 6 7 8 CREATE TABLE `persistent_logins` ( `username` varchar (64 ) NOT NULL , `series` varchar (64 ) NOT NULL , `token` varchar (64 ) NOT NULL , `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP , PRIMARY KEY (`series`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8;
添加数据库的配置文件 1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://192.168.200.128:3306/test username: root password: root
编写配置类 创建配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class BrowserSecurityConfig {@Autowired private DataSource dataSource; @Bean public PersistentTokenRepository persistentTokenRepository () { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl (); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(true ); return jdbcTokenRepository; } }
这个配置类是将token注入到数据库中并且建表,所以执行一次会创建,后来要删除掉,但是这也只是为了实验,后续token可以存在redis中就不用写这个了。
很多现代系统(Vue/React + Spring Boot API)根本不使用 Spring Security 的“记住我”功能 ,而是:
用户登录 → 后端返回 JWT
前端保存 JWT → 每次请求带 Authorization Header
这种架构下,完全不需要 PersistentTokenRepository
所以“后续 token 可以存在 Redis 中就不用写这个”,前提是你的系统仍然在用 Spring Security 的“记住我”机制 。 但如果已经改用 JWT 或 OAuth2,那连“记住我”都不用了,自然也不需要这个配置。
修改安全配置类 1 2 3 4 5 6 7 8 9 10 @Autowired private UsersServiceImpl usersService; @Autowired private PersistentTokenRepository tokenRepository; http.rememberMe() .tokenRepository(tokenRepository) .userDetailsService(usersService);
页面添加记住我复选框 1 记住我:<input type ="checkbox" name ="remember-me" title ="记住密码" /> <br />
此处:name 属性值必须位remember-me.不能改为其他值
使用张三进行登录测试 登录成功之后,关闭浏览器再次访问http://localhost:8090/find,发现依然可以使用!
设置有效期 默认2周时间。但是可以通过设置状态有效时间,即使项目重新启动下次也可以正常登 录。 在配置文件中设置
用户注销 在登录页面添加一个退出连接 success.html
1 2 3 4 <body > 登录成功<br > <a href ="/logout" > 退出</a > </body >
在配置类中添加退出映射地址 1 http.logout().logoutUrl("/logout" ).logoutSuccessUrl("/index" ).permitAll()
CSRF CSRF理解 跨站请求伪造,这个是一种让用户在当前已经登录的Web应用程序上执行非本意操作的攻击方法,跟跨网站脚本XSS相比,XSS利用的是用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。
XSS 是一种攻击者向网页中注入恶意脚本(通常是 JavaScript),当其他用户浏览该页面时,脚本会在其浏览器中执行,从而窃取信息、劫持会话或进行其他恶意操作。XSS也就是在输入框里面输入JS脚本注入到正常页面,然后就会把用户的信息发送到目标服务器。所以需要对输入进行过滤转义或者安全渲染 。
输出转义 :在将用户输入插入 HTML 前,对 < > & " ' 等字符转义(如 < → <)
使用安全 API :避免 innerHTML,改用 textContent
设置 Cookie 的 HttpOnly (防止 JS 读取)
启用 CSP(内容安全策略)
对富文本使用白名单过滤 (如 DOMPurify)
真正的 CSRF 是在恶意页面中“静默提交表单”或“发起 AJAX 请求”到银行,利用浏览器自动携带的 Cookie 冒充用户操作。** 如果银行没有 CSRF 防护(如 Token),这种攻击就会成功。**
假设你已登录银行网站。攻击者构造一个隐藏表单,指向银行的转账接口,并诱导你访问包含该表单的恶意网页。当你打开该网页时,浏览器自动带上你的银行 Cookie,完成转账。
SpringSecurity 实现CSRF的原理 1、生成csrfToken 保存到HttpSession 或者Cookie 中。
SaveOnAccessCsrfToken 类有个接口 CsrfTokenRepository
然后这个接口也有很多实现类。
当前接口实现类:HttpSessionCsrfTokenRepository,CookieCsrfTokenRepository
点进去看就会发现在里面生成_csrf的UUID作为Token
2、然后请求到来时,从请求中提取csrfToken,和保存的csrfToken 做比较,进而判断当 前请求是否合法。主要通过CsrfFilter 过滤器来完成。
SpringSecurity微服务权限方案 1、认证授权过程分析 (1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的session信息,然后判断当前用户是否符合请求的要求。
(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限 信息中去
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于token的形式 进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限 值,并以用户名为key,权限列表为value的形式存入redis缓存中,根据用户名相关信息 生成token返回,浏览器将token记录到cookie中,每次调用api接口都默认将token携带 到header 请求头中,Spring-security 解析 header 头获取 token 信息,解析token 获取当前 用户名,根据用户名就可以从redis中获取权限列表,这样Spring-security就能够判断当前 请求是否有权限访问 。
权限管理数据模型
jwt介绍 1、访问令牌类型 自包含令牌 :令牌自身包含所有必要的信息 (如用户身份、权限、有效期等),资源服务器无需查询外部服务 即可验证和解析令牌。JWT比如。
透明令牌:令牌是一个无意义的随机字符串 (如 a1b2c3d4-e5f6-7890),本身不包含任何信息 。资源服务器必须向授权服务器发起 introspection(内省)请求 才能验证其有效性并获取用户信息。
2、JWT的组成 典型的JWT如下
该对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。 每一个子串表示了一个功能块,总共有以下三个部分:JWT头、有效载荷和签名
JWT头
JWT 头部分是一个描述JWT元数据的JSON对象,通常如下所示。
在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);
typ 属性表示令牌的类型,JWT令牌统一写为JWT。
最后,使用Base64 URL算法将上述 JSON 对象转换为字符串保存。
有效载荷
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:
1 { "sub" : "1234567890" , "name" : "Helen" , "admin" : true }
请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息 字段,存放保密信息,以防止信息泄露。 JSON 对象也使用Base64 URL算法转换为字符串保存。
签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成 签名。
1 HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个 部分用”.”分隔,就构成整个JWT对象。
Base64URL算法
如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算 法类似,稍有差别。 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个 字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换: “=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法。
具体代码实现 用redis存储用户的权限,然后登录成功后从redis获得权限,然后根据用户名生成token,也就是用头记录加密算法,然后有效载荷记录权限信息等,然后签名是服务器公钥通过加密算法生成的签名,然后放到cookie里面然后放在header里面,然后springsecurity就能从token里面获得用户权限,然后赋予权限,然后token是放在浏览器里面也就是客户端里面。
编写核心配置类 Spring Security的核心配置就是继承WebSecurityConfigurerAdapter并注解 @EnableWebSecurity的配置。这个配置指明了用户名密码的处理方式、请求路径、登录 登出控制等和安全相关的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; private final TokenManager tokenManager; private final DefaultPasswordEncoder defaultPasswordEncoder; private final RedisTemplate redisTemplate; @Autowired public TokenWebSecurityConfig ( UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder, TokenManager tokenManager, RedisTemplate redisTemplate) { this .userDetailsService = userDetailsService; this .defaultPasswordEncoder = defaultPasswordEncoder; this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override protected void configure (HttpSecurity http) throws Exception { http .exceptionHandling() .authenticationEntryPoint(new UnauthorizedEntryPoint ()) .and() .csrf().disable() .authorizeRequests() .anyRequest().authenticated() .and() .logout() .logoutUrl("/admin/acl/index/logout" ) .addLogoutHandler(new TokenLogoutHandler (tokenManager, redisTemplate)) .and() .addFilter(new TokenLoginFilter (authenticationManager(), tokenManager, redisTemplate)) .addFilter(new TokenAuthenticationFilter (authenticationManager(), tokenManager, redisTemplate)) .httpBasic(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userDetailsService) .passwordEncoder(defaultPasswordEncoder); } @Override public void configure (WebSecurity web) throws Exception { web.ignoring().antMatchers("/api/**" , "/swagger-ui.html/**" ); } }
创建认证授权相关的工具类
(1)DefaultPasswordEncoder:密码处理的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class DefaultPasswordEncoder implements PasswordEncoder { public DefaultPasswordEncoder () { this (-1 ); } public DefaultPasswordEncoder (int strength) { } public String encode (CharSequence rawPassword) { return MD5.encrypt(rawPassword.toString()); } public boolean matches (CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5.encrypt(rawPassword.toString())); } }
(2)TokenManager:token操作的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Component public class TokenManager { private long tokenExpiration = 24 * 60 * 60 * 1000 ; private String tokenSignKey = "123456" ; public String createToken (String username) { return Jwts.builder() .setSubject(username) .setExpiration(new Date (System.currentTimeMillis() + tokenExpiration)) .signWith(SignatureAlgorithm.HS512, tokenSignKey) .compressWith(CompressionCodecs.GZIP) .compact(); } public String getUserFromToken (String token) { return Jwts.parser() .setSigningKey(tokenSignKey) .parseClaimsJws(token) .getBody() .getSubject(); } public void removeToken (String token) { } }
(3)TokenLogoutHandler:退出实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class TokenLogoutHandler implements LogoutHandler { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLogoutHandler (TokenManager tokenManager, RedisTemplate redisTemplate) { this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override public void logout (HttpServletRequest request, HttpServletResponse response, Authentication authentication) { String token = request.getHeader("token" ); if (token != null ) { tokenManager.removeToken(token); String userName = tokenManager.getUserFromToken(token); redisTemplate.delete(userName); } ResponseUtil.out(response, R.ok()); } }
(4)UnauthorizedEntryPoint:未授权统一处理
1 2 3 4 5 6 7 8 9 10 11 public class UnauthorizedEntryPoint implements AuthenticationEntryPoint { @Override public void commence ( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtil.out(response, R.error()); } }
创建认证授权实体类
(1) SecutityUser
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 @Data @Slf4j public class SecurityUser implements UserDetails { private transient User currentUserInfo; private List<String> permissionValueList; public SecurityUser () { } public SecurityUser (User user) { if (user != null ) { this .currentUserInfo = user; } } @Override public Collection<? extends GrantedAuthority > getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList <>(); for (String permissionValue : permissionValueList) { if (StringUtils.isEmpty(permissionValue)) continue ; SimpleGrantedAuthority authority = new SimpleGrantedAuthority (permissionValue); authorities.add(authority); } return authorities; } @Override public String getPassword () { return currentUserInfo.getPassword(); } @Override public String getUsername () { return currentUserInfo.getUsername(); } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return true ; } }
用户实体类
1 2 3 4 5 6 7 8 9 @Data @ApiModel(description = "用户实体类") public class User implements Serializable { private String username; private String password; private String nickName; private String salt; private String token; }
创建认证和授权filter (1)TokenLoginFilter:认证的filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter { private AuthenticationManager authenticationManager; private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenLoginFilter (AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) { this .authenticationManager = authenticationManager; this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; this .setPostOnly(false ); this .setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher ("/admin/acl/login" , "POST" )); } @Override public Authentication attemptAuthentication (HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { User user = new ObjectMapper ().readValue(req.getInputStream(), User.class); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken (user.getUsername(), user.getPassword(), new ArrayList <>())); } catch (IOException e) { throw new RuntimeException (e); } } @Override protected void successfulAuthentication (HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException { SecurityUser user = (SecurityUser) auth.getPrincipal(); String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername()); redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList()); ResponseUtil.out(res, R.ok().data("token" , token)); } @Override protected void unsuccessfulAuthentication (HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException,ServletException { ResponseUtil.out(response, R.error()); } }
(2)TokenAuthenticationFilter:授权filter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class TokenAuthenticationFilter extends BasicAuthenticationFilter { private TokenManager tokenManager; private RedisTemplate redisTemplate; public TokenAuthenticationFilter (AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) { super (authManager); this .tokenManager = tokenManager; this .redisTemplate = redisTemplate; } @Override protected void doFilterInternal (HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { logger.info("=================" +req.getRequestURI()); if (req.getRequestURI().indexOf("admin" ) == -1 ) { chain.doFilter(req, res); return ; } UsernamePasswordAuthenticationToken authentication = null ; try { authentication = getAuthentication(req); } catch (Exception e) { ResponseUtil.out(res, R.error()); } if (authentication != null ) { SecurityContextHolder.getContext().setAuthentication(authentication); } else { ResponseUtil.out(res, R.error()); } chain.doFilter(req, res); } private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request) { String token = request.getHeader("token" ); if (token != null && !"" .equals(token.trim())) { String userName = tokenManager.getUserFromToken(token); List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName); Collection<GrantedAuthority> authorities = new ArrayList <>(); for (String permissionValue : permissionValueList) { if (StringUtils.isEmpty(permissionValue)) continue ; SimpleGrantedAuthority authority = new SimpleGrantedAuthority (permissionValue); authorities.add(authority); } if (!StringUtils.isEmpty(userName)) { return new UsernamePasswordAuthenticationToken (userName, token, authorities); } return null ; } return null ; } }
SpringSecurity 原理总结 SpringSecurity 的过滤器介绍 SpringSecurity采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤 器链的15个过滤器进行说明:
(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与 Spring Web 中用于 处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上 下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将 SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将 SecurityContextHolder 中的信息清除,例如在Session中维护一个用户的安全信 息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中 获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码 时,默认使用的表单 name 值为 username 和 password,这两个值可以通过设置这个 过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会 配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在 Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和 AuthenticationException 异常。 (14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。
基本流程 Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个 过滤器:
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以 使用Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认 证过滤器要在configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重 点介绍以下三个过滤器:
UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式 的登录表单请求,并进行身份认证。
ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会 直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。
FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源 权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并 由ExceptionTranslationFilter 过滤器进行捕获和处理。
认证流程 认证流程是在UsernamePasswordAuthenticationFilter 过滤器中处理的,具体流程如下 所示:
UsernamePasswordAuthenticationFilter 源码 当前端提交的是一个 POST 方式的登录表单请求,就会被该过滤器拦截,并进行身份认 证。该过滤器的 doFilter() 方法实现在其抽象父类 AbstractAuthenticationProcessingFilter 中,查看相关源码:
上述的 第二 过程调用了UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 方法,源码如下:
上述的(3)过程创建的UsernamePasswordAuthenticationToken 是 Authentication 接口的实现类,该类有两个构造器,一个用于封装前端请求传入的未认 证的用户信息,一个用于封装认证成功后的用户信息:
Authentication 接口的实现类用于存储用户认证信息,查看该接口具体定义:
ProviderManager 源码 上述过程中,UsernamePasswordAuthenticationFilter 过滤器的 attemptAuthentication() 方法的(5)过程将未认证的 Authentication 对象传入 ProviderManager 类的 authenticate() 方法进行身份认证。 ProviderManager 是 AuthenticationManager 接口的实现类,该接口是认证相关的核心接 口,也是认证的入口。在实际开发中,我们可能有多种不同的认证方式,例如:用户名+ 密码、邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是 AuthenticationManager。在该接口的常用实现类 ProviderManager 内部会维护一个 List列表,存放多种认证方式,实际上这是委托者模式 (Delegate)的应用。每种认证方式对应着一个 AuthenticationProvider, AuthenticationManager 根据认证方式的不同(根据传入的 Authentication 类型判断)委托 对应的 AuthenticationProvider 进行用户认证。
上述认证成功之后的(6)过程,调用 CredentialsContainer 接口定义的 eraseCredentials() 方法去除敏感信息。查看 UsernamePasswordAuthenticationToken 实现的 eraseCredentials() 方法,该方 法实现在其父类中:
认证成功 / 失败处理 上述过程就是认证流程的最核心部分,接下来重新回到 UsernamePasswordAuthenticationFilter 过滤器的 doFilter() 方法,查看认证成 功/失败的处理:
SpringSecurity 权限访问流程 上一个部分通过源码的方式介绍了认证流程,下面介绍权限访问流程,主要是对 ExceptionTranslationFilter 过滤器和 FilterSecurityInterceptor 过滤器进行介绍。
ExceptionTranslationFilter 过滤器 该过滤器是用于处理异常的,不需要我们配置,对于前端提交的请求会直接放行,捕获后 续抛出的异常并进行处理(例如:权限访问限制)。具体源码如下:
FilterSecurityInterceptor 过滤器 FilterSecurityInterceptor 是过滤器链的最后一个过滤器,该过滤器是过滤器链 的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果 访问受限会抛出相关异常,最终所抛出的异常会由前一个过滤器
ExceptionTranslationFilter 进行捕获和处理。具体源码如下:
需要注意,Spring Security 的过滤器链是配置在 SpringMVC 的核心组件 DispatcherServlet 运行之前。也就是说,请求通过Spring Security 的所有过滤器, 不意味着能够正常访问资源,该请求还需要通过 SpringMVC 的拦截器链。
SpringSecurity请求间共享认证信息 一般认证成功后的用户信息是通过Session在多个请求之间共享,那么SpringSecurity中是如何实现将已认证的用户信息对象 Authentication 与 Session 绑定的进行具体分析。
在前面讲解认证成功的处理方法 successfulAuthentication() 时,有以下代码:
查看 SecurityContext 接口及其实现类 SecurityContextImpl,该类其实就是对 Authentication 的封装:
查看 SecurityContextHolder 类,该类其实是对 ThreadLocal 的封装,存储 SecurityContext 对象:
SecurityContextHolder :这是Spring Security的核心 。它默认使用ThreadLocal来存储当前请求的安全上下文(SecurityContext)。因为一次请求由一个线程处理,所以在请求的任何地方都能通过它拿到用户信息
SecurityContextPersistenceFilter :这个过滤器在过滤器链的最前端 。它的doFilter方法做了三件事 :
请求前 :从Session中读取SecurityContext,并设置到SecurityContextHolder中。
请求中 :放行请求,让后续的Filter和Servlet处理业务。
请求后 :从SecurityContextHolder中取出SecurityContext(可能已被修改),保存回Session,然后清空SecurityContextHolder。