Security授权
一个最简单的security示例代码
1.1权限认证流程
SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。流程如下图
SecurityContextPersistenceFilter:
———SecurityContext :存储认证用户的上下文对象
———SecurityContextRepository:用来存储SecurityContext 的仓库
———SecurityContextHolder:用来操作SecurityContext 的工具
UsernamePasswordAuthenticationFilter:
———默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证。
BasicAuthenticationFilter:
———基本认证,httpBasic登录,弹出登录框登录
RememberAuthenticationFilter:
———匿名Filter,用来处理匿名访问的资源,如果SecurityContext中没有Authentication,就会创建匿名的Token(AnonymousAuthenticationToken),然后通过SecurityContextHodler设置到SecurityContext中。
FilterSecurityInterceptor:
———用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用AccessDecisionManager.decide方法对用户进行授权。
1.2Security相关概念
Authentication
所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如 最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码。其实就是User对象
AuthenticationManager
用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给 AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会调用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认 证信息的Authentication对象。
AuthenticationProvider.authenticate
认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码我 是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通 过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前 面讲了AuthenticationManager只是一个代理接口,真正的认证就是由 AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider, 每个provider通过实现一个support方法来表示自己支持那种Token的认证。 AuthenticationManager默认的实现类是ProviderManager。
UserDetailService
用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或 内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿 意把它认为是我们系统里经常有的UserDao。
SecurityContext
当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用 户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识 Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过 SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过 SecurityUtils.getSubject()到达同样的目的
1.3.SpringSecurity认证流程原理
认证原理:
1.请求先到一个持久SecurityContext的filter: 从SecurityContextRepository仓库中存,取securitycontxt到securityContextHolder中
2.请求到一个UsernamepasswordAuthenticationFilter:封装表单中的用户名密码成UsernamepasswordAuthenticationToken对象,调用authticationManger做认证.
3.authticationManger调用authenticationProvider做认证
4.authenticationProvider调用userDetailsServer加载数据库中的用户信息(UserDeatils) [对应的是我们简单示例代码配置文件中UserDetailsService方法 ]
5.AuthenticationProvider会调用PasswordEncoder对比传入的密码和数据库查询的密码
6.校验成功,把用户信息又封装成一个UsernamepasswordAuthenticationToken对象,设置给SecurityContext
7.把SecurityContext设置到SecurityContextHolder中
8.持久SecurityContext的filter会从SecurityContextHolder中取出SecurityContext设置到securityContextRepository中存储起来,删除holder中的数据
9.接下来访问资源的时候持久SecurityContext的filter会从securityContextRepository中取出securityContext设置到SecurityContextHolder,以后在代码中直接通过SecurityContextHolder就可以获取认证信息了
2.0定义一个认证流程
在SpringSecurity的整个认证流程中,除了UserDetailsService需要我们自己定义外,其他的的组件都可以使用默认的,因为UserDetailsService是SpringSecurity获取数据库中的认证信息的媒介,而如何才能从数据库中获取认证信息只有我们才知道。在入门案例中我们使用的是InMemoryUserDetailsManager 基于内存的UserDetailsService方案,接下来我们需要把基于内存的方案修改为基于数据库的方案。
2.1首先我们需要定义一个UserDetailsService
1.我们去实现UserDetailsService 这个类覆写loadUserByUsername
2.我们去数据库将用户及用户权限信息查询到并封装到我们的UserDetails中(也就是需要返回的类)但是因为是interface修饰的所以我们用其子类User类进行封装
这是需要传入的数据 GrantedAuthority 用来封装权限信息
package cn.zhangqiang.userdetials;
import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.domain.VipUser;
import cn.zhangqiang.mapper.PermissionMapper;
import cn.zhangqiang.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//查询到当前用户 将当前用户的信息封装成一个UserDetails对象进行返回
VipUser user = userMapper.selectByUsername(s);
//查询到所有权限
List<Permission> permissions = permissionMapper.selectPermissionsByUserId(user.getId());
//将Permission对象转换为SimpleGrantedAuthority对象
//new SimpleGrantedAuthority();
List<SimpleGrantedAuthority> collect = permissions.stream().map(permission -> {
return new SimpleGrantedAuthority(permission.getExpression());
}).collect(Collectors.toList());
//String username, String password, boolean enabled, boolean accountNonExpired,
// boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities
UserDetails userDetails = new User(user.getUsername(),user.getPassword(),user.isEnabled(),user.isAccount_non_expired(),
user.isCredentials_non_expired(),user.isAccount_non_locked(),collect);
return userDetails;
}
}
2.2Yml配置
spring:
datasource:
url: jdbc:mysql:///hrm-security
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
mybatis:
mapper-locations: classpath:cn/zhangqiang/mapper/*Mapper.xml
2.3认证成功处理
自定义类实现AuthenticationSuccessHandler接口复写 onAuthenticationSuccess方法,该方法其中一个参数是Authentication ,他里面封装了认证信息,用户信息UserDetails等,我们需要在这个方法中使用Response写出json数据即可
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
Map map = new HashMap<>();
map.put("success",true);
map.put("message","认证成功");
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
需要导入一个依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.50</version>
</dependency>
2.4认证失败处理
自定义登录失败的处理,需要实现AuthenticationFailureHandler接口,复写onAuthenticationFailure方法实现自己的认证失败结果处理
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
Map map = new HashMap<>();
map.put("success",false);
map.put("message","认证失败");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().print(JSON.toJSONString(map));
response.getWriter().flush();
response.getWriter().close();
}
}
2.5授权失败处理
package cn.zhangqiang.handel;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@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.6定义身份入口点
package cn.zhangqiang.handel;
import com.alibaba.fastjson.JSON;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@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();
}
}
2.7对config文件进行配置
package cn.zhangqiang.config;
import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.handel.DefaultAccessDeniedHandler;
import cn.zhangqiang.handel.MyAuthenticationEntryPoint;
import cn.zhangqiang.handel.MyAuthenticationFailureHandler;
import cn.zhangqiang.handel.MyAuthenticationSuccessHandler;
import cn.zhangqiang.mapper.PermissionMapper;
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;
@SuppressWarnings("ALL")
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//因为基于数据库所以这个就不要了
// //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
// @Bean
// public UserDetailsService userDetailsService(){
//
// InMemoryUserDetailsManager inMemoryUserDetailsManager =
// new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
//
//
//
// return inMemoryUserDetailsManager;
// }
@Autowired
private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private PermissionMapper permissionMapper;
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder(){
//return NoOpPasswordEncoder.getInstance();
//切换一个加密方式
return new BCryptPasswordEncoder();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//这是循环进行授权 (这些数据都是来自于数据库)
List<Permission> permissions = permissionMapper.selectAll();
permissions.forEach(permission -> {
try {
http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
log.info("为资源授权{} -> {}",permission.getResource(),permission.getExpression());
} catch (Exception e) {
e.printStackTrace();
}
//http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
});
//授权失败处理
http.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint)
.accessDeniedHandler(defaultAccessDeniedHandler);
http.authorizeRequests() //授权配置
.antMatchers("/login","/login.html").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successHandler(myAuthenticationSuccessHandler) //认证成功结果处理
.failureHandler(myAuthenticationFailureHandler) //认证结果失败处理
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.loginPage("/login.html") //登录页面地址
.loginProcessingUrl("/login") //登录提交地址
.and().logout().permitAll() //登出路径放行 /logout
.and().csrf().disable(); //关闭跨域伪造检查
}
}
扩展–security的记住我功能实现
需要自定义的登录页中name=“remember-me” 这是约定
<div class="checkbox">
<label><input type="checkbox" id="rememberme" name="remember-me"/>记住我</label>
</div>
配置文件WebSecurityConfig修改
package cn.zhangqiang.config;
import cn.zhangqiang.domain.Permission;
import cn.zhangqiang.handel.DefaultAccessDeniedHandler;
import cn.zhangqiang.handel.MyAuthenticationEntryPoint;
import cn.zhangqiang.handel.MyAuthenticationFailureHandler;
import cn.zhangqiang.handel.MyAuthenticationSuccessHandler;
import cn.zhangqiang.mapper.PermissionMapper;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
import java.util.List;
@SuppressWarnings("ALL")
@Configuration
@EnableWebSecurity
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// //提供用户信息,这里没有从数据库查询用户信息,在内存中模拟
// @Bean
// public UserDetailsService userDetailsService(){
//
// InMemoryUserDetailsManager inMemoryUserDetailsManager =
// new InMemoryUserDetailsManager();
// inMemoryUserDetailsManager.createUser(User.withUsername("zs").password("123").authorities("admin").build());
//
//
//
// return inMemoryUserDetailsManager;
// }
@Autowired
private DefaultAccessDeniedHandler defaultAccessDeniedHandler;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private PermissionMapper permissionMapper;
//记住我需要
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl obj = new JdbcTokenRepositoryImpl();
obj.setDataSource(dataSource);
//obj.setCreateTableOnStartup(true);
//启动创建表persistent_logs表,存token,username时会用到
//数据库中没有表的时候启用一下如果有表第二次启动会报错,所以启动以后会注释掉
return obj;
}
//密码编码器:不加密
@Bean
public PasswordEncoder passwordEncoder(){
//return NoOpPasswordEncoder.getInstance();
//切换一个加密方式
return new BCryptPasswordEncoder();
}
//授权规则配置
@Override
protected void configure(HttpSecurity http) throws Exception {
//这是循环进行授权 (这些数据都是来自于数据库)
List<Permission> permissions = permissionMapper.selectAll();
permissions.forEach(permission -> {
try {
http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
log.info("为资源授权{} -> {}",permission.getResource(),permission.getExpression());
} catch (Exception e) {
e.printStackTrace();
}
//http.authorizeRequests().antMatchers(permission.getResource()).hasAuthority(permission.getExpression());
});
//授权失败处理
http.exceptionHandling()
.authenticationEntryPoint(myAuthenticationEntryPoint) //-----------------------------------------------------
.accessDeniedHandler(defaultAccessDeniedHandler); //-----------------------------------------------------
//记住我
http.rememberMe()
.tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(360000)
.userDetailsService(userDetailsService);
http.authorizeRequests() //授权配置
.antMatchers("/login","/login.html").permitAll() //登录路径放行
.anyRequest().authenticated() //其他路径都要认证之后才能访问
.and().formLogin() //允许表单登录
.successHandler(myAuthenticationSuccessHandler) //认证成功结果处理 ------------------------------------------------------
.failureHandler(myAuthenticationFailureHandler) //认证结果失败处理 -------------------------------------------------------
.successForwardUrl("/loginSuccess") // 设置登陆成功页
.loginPage("/login.html") //登录页面地址
.loginProcessingUrl("/login") //登录提交地址
.and().logout().permitAll() //登出路径放行 /logout
.and().csrf().disable(); //关闭跨域伪造检查
}
}
OK完成 点击记住我 退出后依然能直接访问到自己的个人页面不需要登录
有需要的可以pl找我拿数据库的sql,和整个测试demo