依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
基本原理
过滤器链
SpringSecurity 本质是一个过滤器链,有三个重要的过滤器:
- FilterSecurityInterceptor:是一个方法级的权限过滤器, 基本位于过滤链的最底部。
- ExceptionTranslationFilter:是个异常过滤器,用来处理在认证授权过程中抛出的异常
- UsernamePasswordAuthenticationFilter :对/login 的 POST 请求做拦截,校验表单中用户名,密码
两个接口
UserDetailsService 接口
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
如果需要自定义逻辑时,只需要实现 UserDetailsService 接口即可。接口定义如下:
publicinterfaceUserDetailsService {
UserDetailsloadUserByUsername(Stringvar1) throwsUsernameNotFoundException;
}
返回值 UserDetails ,这个类是系统默认的用户“主体”。接口实现类User
// 表示获取登录用户所有权限
Collection<?extendsGrantedAuthority>getAuthorities();
// 表示获取密码
StringgetPassword();
// 表示获取用户名
StringgetUsername();
// 表示判断账户是否过期
booleanisAccountNonExpired();
// 表示判断账户是否被锁定
booleanisAccountNonLocked();
// 表示凭证{密码}是否过期
booleanisCredentialsNonExpired();
// 表示当前用户是否可用
booleanisEnabled();
PasswordEncoder 接口
// 表示把参数按照特定的解析规则进行解析
Stringencode(CharSequencerawPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
参数表示存储的密码。
booleanmatches(CharSequencerawPassword, StringencodedPassword);
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回false。默认返回 false。
defaultbooleanupgradeEncoding(StringencodedPassword) {
returnfalse;
}
接口实现类
BCryptPasswordEncoder 是 Spring Security 官方推荐的密码解析器,平时多使用这个解析器。
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单向加密。可以通过 strength 控制加密强度,默认 10
使用示例:
@Test
publicvoidtest01(){
// 创建密码解析器
BCryptPasswordEncoderbCryptPasswordEncoder=newBCryptPasswordEncoder();
// 对密码进行加密
Stringatguigu=bCryptPasswordEncoder.encode("atguigu");
// 打印加密之后的数据
System.out.println("加密之后数据:\t"+atguigu);
//判断原字符加密后和加密之前是否匹配
booleanresult=bCryptPasswordEncoder.matches("atguigu", atguigu);
// 打印比较结果
System.out.println("比较结果:\t"+result);
}
Web 权限方案
- 认证
- 授权
查询数据库进行认证
- 编写userDetailsService接口的实现类并装载进容器,返回User对象,User对象有用户名密码和操作权限
- 创建配置类,使用返回的User对象进行认证
packagecn.upeveryday.service.impl;
@Service("userDetailsService")
publicclass_MyUserDetailsServiceImplimplementsUserDetailsService {
@Autowired
privateUserAuthMapperuserAuthMapper;
List<GrantedAuthority>auths=AuthorityUtils.commaSeparatedStringToAuthorityList("role");
@Override
publicUserDetailsloadUserByUsername(Stringusername) throwsUsernameNotFoundException {
//根据用户名查询数据库
QueryWrapperwrapper=newQueryWrapper();
wrapper.eq("username", username);
UserAuthuserAuth=userAuthMapper.selectOne(wrapper);
//判断
if (userAuth==null){//数据库无此用户名,认证失败
thrownewUsernameNotFoundException("用户名不存在!");
}
//权限参数不能为null
returnnewUser(userAuth.getUsername(), newBCryptPasswordEncoder().encode(userAuth.getPassword()), auths);
}
}
packagecn.upeveryday.config;
@Configuration
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter {
//注入我们写的实现类,内有用户名密码和操作权限
@Autowired
privateUserDetailsServiceuserDetailsService;
@Override
protectedvoidconfigure(AuthenticationManagerBuilderauth) throwsException {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
/**
* 将密码加密器装载进容器
* @return
*/
@Bean
PasswordEncoderpassword(){
returnnewBCryptPasswordEncoder();
}
}
其他配置
同样在配置类中
@Override
protectedvoidconfigure(HttpSecurityhttp) throwsException {
// 配置认证
http.formLogin()
.loginPage("/index") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login") // 设置哪个是登录的 url。
.successForwardUrl("/success") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/layui/**","/index") //表示配置请求路径
.permitAll() // 指定 URL 无需保护。
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭 csrf
http.csrf().disable();
}
基于角色或权限进行访问控制
hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true,否则返回 false
- 修改配置类
@Override
protectedvoidconfigure(HttpSecurityhttp) throwsException {
// 配置认证
http.formLogin()
.loginPage("/index") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login") // 设置哪个是登录的 url。
.successForwardUrl("/success") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail");// 登录失败之后跳转到哪个 url
http.authorizeRequests()
.antMatchers("/layui/**","/index").permitAll() //指定的 URL 无需保护
.antMatchers("/test").hasAuthority("admin")//当前登录用户,只有具有admin权限才能访问这个路径
.anyRequest() // 其他请求
.authenticated(); //需要认证
// 关闭 csrf
http.csrf().disable();
}
hasAnyAuthority 方法
满足任意一个权限即可访问
@Override
protectedvoidconfigure(HttpSecurityhttp) throwsException {
// 配置认证
http.formLogin()
.loginPage("/index")
.loginProcessingUrl("/login")
.successForwardUrl("/success")
.failureForwardUrl("/fail");
http.authorizeRequests()
.antMatchers("/layui/**","/index").permitAll()
// .antMatchers("/test").hasAuthority("admin")
.antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径
.anyRequest()
.authenticated();
http.csrf().disable();
}
hasRole 方法
如果当前主体具有指定的角色,则返回 true。
底层源码:
privatestaticStringhasRole(Stringrole) {
Assert.notNull(role, "role cannot be null");
Assert.isTrue(!role.startsWith("ROLE_"), () -> {
return"role should not start with 'ROLE_' since it is automatically inserted. Got '"+role+"'";
});
return"hasRole('ROLE_"+role+"')";
}
注意:这里与添加权限不同,添加角色需要加上"ROLE_"前缀
admin是权限
ROLE_sale是角色
@Service("userDetailsService")
publicclass_MyUserDetailsServiceImplimplementsUserDetailsService {
@Autowired
privateUserAuthMapperuserAuthMapper;
List<GrantedAuthority>auths=AuthorityUtils.commaSeparatedStringToAuthorityList("admin,ROLE_sale");
……
}
注意配置文件中不需要添加”ROLE_“,因为上述的底层代码会自动添加与之进行匹配
@Override
protectedvoidconfigure(HttpSecurityhttp) throwsException {
// 配置认证
http.formLogin()
.loginPage("/index")
.loginProcessingUrl("/login")
.successForwardUrl("/success")
.failureForwardUrl("/fail");
http.authorizeRequests()
.antMatchers("/layui/**","/index").permitAll()
// .antMatchers("/test").hasAuthority("admin")
// .antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径
.antMatchers("/test").hasRole("sale")
.anyRequest()
.authenticated();
http.csrf().disable();
}
hasAnyRole方法
表示用户具备任何一个条件都可以访问。
注解使用
@Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
使用注解先要开启注解功能!
在启动类或配置类添加注解都可
@SpringBootApplication
@MapperScan("cn.upeveryday.mapper")
@EnableGlobalMethodSecurity(securedEnabled=true)//开启security的注解功能
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
在控制器方法上添加注解
@RequestMapping("testSecured")
@ResponseBody
@Secured({"ROLE_normal","ROLE_admin"})
public String helloUser() {
return "hello,user";
}
@PreAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
四个方法都可使用
@RequestMapping("/preAuthorize")
@ResponseBody
//@PreAuthorize("hasRole('ROLE_管理员')")
@PreAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}
@PostAuthorize
先开启注解功能:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限.
@RequestMapping("/testPostAuthorize")
@ResponseBody
@PostAuthorize("hasAnyAuthority('menu:system')")
public String preAuthorize(){
System.out.println("test--PostAuthorize");
return "PostAuthorize";
}
@PostFilter
@PostFilter :权限验证之后对数据进行过滤 留下用户名是 admin1 的数据
表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
@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: 进入控制器之前对数据进行过滤
@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;
}
用户注销
在配置类中添加退出映射地址
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll();
自动登录
创建表
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;
修改安全配置类
package cn.upeveryday.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入我们写的实现类,内有用户名密码和操作权限
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
/**
* 将密码加密器装载进容器
* @return
*/
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
//---------------------------------------------------------------------------
//注入yaml配置的数据源(记住我的操作)
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new
JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置认证
http.formLogin()
.loginPage("/index")
.loginProcessingUrl("/login")
.successForwardUrl("/success")
.failureForwardUrl("/fail");
http.authorizeRequests()
.antMatchers("/layui/**","/index").permitAll()
// .antMatchers("/test").hasAuthority("admin")
// .antMatchers("/test").hasAnyAuthority("admin,manager")//当前登录用户,具有其中一个权限即可访问这个路径
.antMatchers("/test").hasRole("sale")
.anyRequest()
.authenticated()
//设置记住我的操作
.and().rememberMe().tokenRepository(persistentTokenRepository())//s
.userDetailsService(userDetailsService)//指定用户
.tokenValiditySeconds(60);//设置有效时长,单位s
}
}
页面添加记住我复选框
此处:name 属性值必须位 remember-me.不能改为其他值
记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>
登录结果返回json
由于前后端分离,我们登录成功或失败,只需返回json数据
步骤:
导入fastjson依赖
<!--fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
编写配置类
- cn.upeveryday.config.SecurityConfig
认证过程需要successHandler和failureHandler,查看源码,分别是AuthenticationSuccessHandler接口和AuthenticationFailureHandler接口
package cn.upeveryday.config;
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//注入我们写的实现类,内有用户名密码和操作权限
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
/**
* 将密码加密器装载进容器
* @return
*/
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
//--------------------------------------------
//注入处理器
@Autowired
private AuthenticationSuccessHandlerImpl authenticationSuccessHandler;
@Autowired
private AuthenticationFailHandlerImpl authenticationFailHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 对认证进行配置
http.formLogin()
.loginProcessingUrl("/login")
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailHandler)
;
http.csrf().disable();
}
}
编写接口实现类
- cn.upeveryday.handler.AuthenticationSuccessHandlerImpl
package cn.upeveryday.handler;
/**
* 登录成功处理
*/
@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//返回json数据
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(Result.success().message(ResultInfo.LOGIN_SUCCESS)));
}
}
- cn.upeveryday.handler.AuthenticationFailHandlerImpl
package cn.upeveryday.handler;
/**
* 登录失败处理
*/
@Component
public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
//返回json数据
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(Result.error().message(ResultInfo.LOGIN_FAILURE)));
}
}
在module中通过mapState、mapGetters、mapActions和mapMutations等辅助函数来绑定要触发的函数