SpringSecurity简介
最下面有与springboot整合的模块代码
用户认证和用户授权
- 主要包含两部分:用户认证和用户授权
- 用户认证:进入用户登录时候,输入用户名密码,查询数据库查看是否正确,如果正确,则认证成功
- 用户授权:登陆了系统,登录用户可能是不同的角色,比如普通用户和管理员
- springsecurity本质上就是用filter对请求的路径进行过滤
- 如果是基于Session,则会对cookie里的sessionId进行解析,找到服务器存储的session信息,然后判断用户是否符合请求的要求
- 如果是token,则解析出token,然后将当前请求加入到springsecurity管理的权限信息中去
认证与授权实现思路
如果系统的模块众多,每个模块都需要进行授权与认证,所以选用token的形式进行授权与认证比较方便,用户根据用户名密码认证成功,然后返回当前用户角色的一系列权限值,并以用户名为key,权限列表为value存入redis中,根据用户相关信息返回token,浏览器将token记录到cookie中,每次调用接口都默认将token携带到header请求头中,springsecurity会解析header头获取token信息,解析token获取当前用户名,根据用户名就能够从中获取权限列表,这样就能够完成权限控制
同类型产品对比
SpringSecurity特点:
- 和Spring无缝整合
- 全面的权限控制
- 转为web开发而设计
- 旧版不能脱离web环境使用
- 新版对整个框架进行了分层抽离,分成了核心模块和web模块。单独引入核心模块则能脱离web环境
- 重量级
shiro特点:
- 轻量级,shiro主张把复杂的简单化。针对性能要求高的互联网应用有更好表现
- 通用性
- 好处:不局限于web环境
- 缺陷:在web环境下一些特定需求需要手动编写代码控制
模块划分
基本原理
本质上是一个过滤器链
从启动是可以获取到过滤器链:
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
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 请求做拦截
,校验表单中用户 名,密码。
两个重要接口
UserDetailsService
查询数据库用户名密码的过程
当什么也没写时只引入了springsecurity时,账号密码都是由框架生成的,而在实际中都是从数据库中查询出来。所以需要自定义逻辑控制认证逻辑
- 创建类继承UsernamePasswordAuthenticationFilter ,重写三个方法(判断用户名密码是否成功的过程)
- 创建类实现UserDetailsService,编写查询数据过程,返回User对象,这个User对象是安全框架提供的
PasswordEncoder
数据加密接口,用于返回User对象里面密码加密
// 表示把参数按照特定的解析规则进行解析
String encode(CharSequence rawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
参数表示存储的密码。
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
false。默认返回 false。
default boolean upgradeEncoding(String encodedPassword) {
return false; }
web权限方案
用户认证
设置登录的用户名和密码的三种方式
配置文件
server.port=8111
spring.security.user.name=atguigu
spring.security.user.password=atguigu
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encode = passwordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("xyouzi").password(encode).roles("admin");
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
自定义实现类
-
创建配置类,设置使用userDetailService实现类
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder()); } @Bean PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
-
编写实现类,返回User对象,User对象有用户名密码和操作权限
@Service public class MyUserDetailService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("role"); return new User("xyouzi",new BCryptPasswordEncoder().encode("123"),role); } }
从数据库中查询认证
user表
添加依赖
<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>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
实体类
@Data
public class Users {
private Integer id;
private String username;
private String password; }
整合 MybatisPlus
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
配置文件
server.port=8111
#mysql 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
制作登录实现类
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// username是前端传来的用户名,密码则由springsecurity内部帮你判断了
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
System.out.println(user);
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),role);
}
}
断点调试
自定义登录页面
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
login.html
name="username"和name="password"不能修改
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head lang="en">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>xx</title>
</head>
<body>
<h1>表单提交</h1>
<form action="/user/login" method="post">
<input type="text" name="username" />
<input type="text" name="password" />
<input type="submit" />
</form>
</body>
</html>
配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 自定义登录页面
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功跳转路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护
}
}
Controller
通过上面的配置类设定后,访问/test/hello不需要用户名密码认证即可直接进入
访问/test/index则需要先通过认证,并且/test/index是登录成功后跳转的路径
@RestController
@RequestMapping("/test")
public class SecurityController {
@GetMapping("hello")
public String hello() {
return "hello security";
}
@GetMapping("index")
public String index() {
return "hello index";
}
}
授权
权限控制
hasAuthority方法
如果当前主体具有指定的权限,则返回true,否则返回false
在配置类中添加设置当前访问地址有哪些权限
.antMatchers("/test/index").hasAuthority("admins") // 只有admins权限才可访问/test/index
在MyUserDetailService类中,把返回的User对象设置权限
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),role);
完整代码
MyUserDetailService
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Users user = usersMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
System.out.println(user);
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
return new User(user.getUsername(),new BCryptPasswordEncoder().encode(user.getPassword()),role);
}
}
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 自定义登录页面
.loginPage("/login.html") // 登录页面设置
.loginProcessingUrl("/user/login") // 登录访问路径
.defaultSuccessUrl("/test/index").permitAll() // 登录成功跳转路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
.antMatchers("/test/index").hasAuthority("admins") // 只有admins权限才可访问/test/index
.anyRequest().authenticated()
.and().csrf().disable(); // 关闭csrf防护
}
}
hasAnyAuthority方法
.antMatchers("/test/index").hasAnyAuthority("admins,manager") //设置多个权限可访问/test/index
角色控制
hasRole方法
如果当前主体具有指定的角色,则返回true,否则返回false
在配置类中添加设置当前访问地址有哪些角色
.antMatchers("/test/index").hasRole("sale")
hasAnyRole方法
.antMatchers("/test/index").hasAnyRole("sale,salee")
自定义403页面
配置类中添加并新建403页面unauth.html,命名可修改
http.exceptionHandling().accessDeniedPage("/unauth.html");
认证授权注解使用
@Secured
用户具有某个角色,才可以访问该注解下的方法
- 启动类开启注解
@EnableGlobalMethodSecurity(securedEnabled=true)
-
在controller里需要添加角色控制的方法上使用注解,设置角色
只有角色为ROLE_sale的用户才能访问该方法
@Secured({
"ROLE_sale"})
@GetMapping("update")
public String update() {
return "hello update";
}
-
在userDetailService设置角色
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("manager,ROLE_sale");
@PreAuthorize
进入方法之前验证你有没有该权限
-
启动类开启注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
-
在controller里需要添加角色控制的方法上使用注解
@PreAuthorize("hasAnyAuthority('manager')") @GetMapping("update") public String update() { return "hello update"; }
-
在userDetailService设置角色
List<GrantedAuthority> role = AuthorityUtils.commaSeparatedStringToAuthorityList("manager,ROLE_sale");
用户注销
在登录页面添加一个退出连接按钮
<body>
登录成功<br> <a href="/logout">退出</a>
</body>
在配置类中添加退出映射地址
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll