SpringSecurity:一个能够为 基于Spring的企业应用系统 提供声明式的安全访问控制解决方案的安全框架,主要两个功能:认证和授权。
基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。
===================================================
那么其中认证流程是这样的。。。
P.S:传统认证流程,如果前端页面使用的是html的话,那么是需要自己做一个类似session的机制,因为html不似jsp(有内置session),所以后端查询到的用户信息只能存在LocalStorage或者SessionStorage,前端要弄前置拦截器,每次发送请求的时候携带Token,然后回调的时候再在后置拦截器中判断
我的理解是这样的
而授权流程是这样的。。。
有两种:web授权和注解授权
1.1 web授权:在Security配置类中,通过HttpSecurity.authorizeRequests()给资源指定访问的权限
1.2 注解授权:在service,controller等的方法上贴注解进行授权,即在方法上指定该方法需要什么样的权限才能访问。
有@Secured、@PreAuthorize、@PostAuthorize;
其中@Secured与@PreAuthorize效果一样,但@PreAuthorize更强大,更简洁;而@PostAuthorize是后置授权。。。有啥用???
使用前提:需要在配置类上开启授权注解支持;@EnableGlobalMethodSecurity(securedEnabled=true)
1.2.1 @Secured
@Secured(“IS_AUTHENTICATED_ANONYMOUSLY”) :方法可以匿名访问
@Secured(“ROLE_DEPT”) ,需要拥有部门的角色才能访问,ROLE_前缀是固定的
1.2.2 @PreAuthorize
@PreAuthorize(“isAnonymous()”) : 方法匿名访问
@PreAuthorize(“hasAnyAuthority(‘p_user_list’,‘p_dept_list’)”) :拥有p_user_listr或者p_dept_list的权限能访问
@PreAuthorize(“hasAuthority(‘p_transfer’) and hasAuthority(‘p_read_accout’)”) : 拥有p_transfer权限和p_read_accout权限才能访问.
该标签不需要有固定的前缀。
不管是认证还是授权,都是holder从SecurityContext中的Authentication拿信息,因为它里面既有用户的基本信息,也有用户的权限信息
上案例:认证+授权
1、Security的配置类:加密器(BCryptPasswordEncoder)+配置规则
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.List;
@Configuration //告诉Spring这是一个配置类
@EnableWebSecurity //开启Security
@Slf4j //打印
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 提供用户信息,如果不从数据库查询用户信息,就在内存中模拟
*/
// @Bean //因为在认证流程中DaoAuthentication就是调UserDetailsService来查数据库中的用户信息
// public UserDetailsService userDetailsService(){
// InMemoryUserDetailsManager inMemoryUserDetailsManager =
// new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
// return inMemoryUserDetailsManager;
// }
@Autowired
private PermissionMapper permissionMapper;
//密码编码器
@Bean
public PasswordEncoder passwordEncoder(){
//return NoOpPasswordEncoder.getInstance();//不加密
//很厉害的密码加密器,每次加密同一个字符串的结果不同(因为每次它内部自己会加不同的盐)
//这样我数据库中的user就只需要password字段来保存最后的加密字符串,不用记录盐值了
//且反过来这些不同的加密字符串能匹配原始未加密字符串,有效解决了MD5撞库
return new BCryptPasswordEncoder();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
List<Permission> permissions = permissionMapper.selectAll();
permissions.forEach(permission -> {
//打印康康
log.info(permission.getResource()+" : "+permission.getExpression());
try {
//通过循环给数据库中的每个操作进行授权,这叫web授权
http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
} catch (Exception e) {
e.printStackTrace();
}
});
//权限配置
http.authorizeRequests()
//浏览器访问/login,是springSecurity自带的页面
.antMatchers("/login","/login.html").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.loginProcessingUrl("/login") //因为使用的是自定义页面,所以这里要设置表单提交到controller层的哪个api,这里就是提交到SpringSecurity提供的/login,让它去做验证
.and().logout().permitAll() //登出路径放行 /logout
.and().csrf().disable(); //关闭跨域伪造检查,开发阶段嘛;上线阶段可以开启
}
}
2、写一个实现UserDetailsService的类
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 在security的认证流程中会调用UserDetailsService,用来提供给security的用户信息的service,
* 需要复写 loadUserByUsername 方法返回数据库中的用户信息
*/
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
/**
* @param username: 该方法把username传入进来,我们通过username查询用户的信息
(用户,密码,权限列表等)然后封装成 UserDetails进行返回 ,交给security 。
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//------------------------------认证--------------------------------------
VipUser userFromMysql = userMapper.selectByUsername(username);
if(userFromMysql == null){
throw new UsernameNotFoundException("无效的用户名");
}
//------------------------------授权--------------------------------------
//给这个user加上自己的权限
//就不用像以前那样每次去数据库查当前用户有没有当前路径的权限了
List<Permission> permissionsList = permissionMapper.selectPermissionsByUserId(userFromMysql.getId());
//List<Permission> permissionsList 是数据库里查的,但是User的构造器里的参数需要的是List<GrantedAuthority>
//所以这里做一个映射
List<SimpleGrantedAuthority> permissions=permissionsList.stream().map(permission -> new SimpleGrantedAuthority(permission.getExpression()))
.collect(Collectors.toList());
//密码是基于BCryptPasswordEncoder加密的密文
//User是security内部的对象,UserDetails的实现类,用来封装用户的基本信息(用户名,密码,权限列表)
return new User(username,userFromMysql.getPassword(),permissions);
}
}
P.S:这里要注意,返回的User可不是自己的实体类,而是UserDetails 的子类org.springframework.security.core.userdetails.User;
平时编程的习惯domain也叫User,那么这个类中就会出现同名不同类的2个User,如果不想改自己domain的名字,后面返回的User只能用全限定名哦~
以及,前端页面的name一定要是username、password
===================================================================
其他:
1、认证结果处理
1.1 成功
import com.alibaba.fastjson.JSON;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
//认证成功结果处理器
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//authentication: 用户的认证信息对象
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",true);
map.put("message","认证成功");
map.put("data",authentication);
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
1.2 失败
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",false);
map.put("message","认证失败:"+exception.getMessage());
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
在Security的配置类的configure()中加上
http.authorizeRequests() //授权配置
//.successForwardUrl("/loginSuccess") // 有对认证成功结果的处理,那么就不用设置登陆成功页了
.successHandler(myAuthenticationSuccessHandler) //认证成功结果处理
.failureHandler(myAuthenticationFailureHandler) //认证失败处理
2、授权失败
2.1 AccessDeineHandler 用来解决认证过的用户访问无权限资源时的异常
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
@Component
public class DefaultAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",false);
map.put("message","没有权限:"+accessDeniedException.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
2.2 AuthenticationEntryPoint 用来解决匿名用户访问无权限资源时的异常
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
Map map = new HashMap<>();
map.put("success",false);
map.put("message","没有权限:"+e.getMessage());
response.setStatus(HttpStatus.FORBIDDEN.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
然后在Security的配置类configure()中加上
//授权失败处理
http.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(defaultAccessDeniedHandler);
3、Remember me
SpringSecurity提供RememberMeAuthenticationFilter过滤器来实现记住我功能,流程
页面记住我的选择框,注意name一定是remember-me
<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
</div>
然后在Security的配置类中加上仓库
@Autowired
private DataSource dataSource ;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();
obj.setDataSource(dataSource);
//根据JdbcTokenRepositoryImpl中sql,自动创建表persistent_logs,存token,username,就不用再手动创建最后一步的表
//但是这句只能在第一次启动用,之后就应该注释掉
//因为那个sql中是直接创建表,没有判断存不存在,所以再次创建是会报错的
obj.setCreateTableOnStartup(true);
return obj;
}
然后在Security的配置类中,把userDetailsService注入进来,再在configure()中加上
@Autowired
private UserDetailsService userDetailsService;
http.rememberMe()
.tokenRepository(persistentTokenRepository()) //持久
.tokenValiditySeconds(3600) //过期时间
.userDetailsService(userDetailsService); //用来加载用户认证信息的
最后,数据库中应该有这么一张表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL DEFAULT '',
`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;
===================================================================