Spring Security
hello world
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
访问http://localhost:8080/login.html,或其他页面,都会跳转到login.html
username=user(默认)
password=控制台会打印(默认)
自定义:
spring.security.user.name=bjsxt
spring.security.user.password=bjsxt
2.自定义业务层实现类实现UserDetailsService接口,重写loadUserByUsername方法
//如果通过用户名没有查询到对应的数据,应该抛出UsernameNotFoundException,系统就知道用户名没有查询到。
//记得加@Service注解!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@Service
public class serviceImpl implements UserDetailsService {
@Autowired
private AdminMapper adminMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("查询用户中-----------------------");
Admin admin = adminMapper.selectByName(username);
if(admin == null){
throw new UsernameNotFoundException("用户名不存在");
}
//org.springframework.security.core.userdetails.User别和自己定义的User弄混
//此处的用户名应该是客户端传递过来的用户名。而密码应该是从数据库中查询出来的密码。Spring Security会根据User中的password和客户端传递过来的password进行比较。如果相同则表示认证通过,如果不相同表示认证失败。
UserDetails user = new User(admin.getUsername(),admin.getPassword(), AuthorityUtils.NO_AUTHORITIES);
return user;
}
}
3.配置密码解析器
Spring Security要求容器中必须有PasswordEncoder实例。所以当自定义登录逻辑时要求必须给容器注入PaswordEncoder的bean对象
推荐使用的是BCrypt算法的BCryptPasswordEncoder。
记得在类上加@Configuration
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
4.编写持久层和控制器层
要么加@Mapper要么在启动类上加@MapperScan(“包名”)
public interface AdminMapper {
@Select("select * from user where username = #{name}")
public Admin selectByName(@Param("name")String username);
}
controller
@RestController
public class UserController {
@RequestMapping("/index")
public String func(){
return "hello world";
}
}
访问localhost:8080/index就会跳转到localhost:8080/login输入默认密码即可访问index浏览器打印hello world
Spring Security配置类
@SpringBootConfiguration
public class Myconfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//登录及表单提交配置
httpSecurity.formLogin()
.usernameParameter("user")//默认username,就是表单中input中的name属性值
.passwordParameter("pwd")//默认password,就是表单中input中password的name值
.loginPage("/MyLogin")//表单页面
.loginProcessingUrl("/abc")//(默认/login)表单提价对应控制器执行,即form表单active的地址
.defaultSuccessUrl("/successForwardUrl")//重定向指定登录成功后的跳转页面get
// .successForwardUrl("/success")//转发指定登录成功后的跳转页面//只能用post
;
//拦截url
httpSecurity.authorizeRequests()
.antMatchers("/MyLogin").permitAll()指定放行的路径(例如登录和 登出跳转 路径)
.anyRequest().authenticated();//拦截所有路径
//关闭csrf防护
httpSecurity.csrf().disable();
return httpSecurity.build();
}
//密码解析器
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
自定义登录成功处理器:
编写继承AuthenticationSuccessHandler的实现类
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//Principal 主体,存放了登录用户的信息
User user = (User)authentication.getPrincipal();
System.out.println(user.getUsername());
System.out.println(user.getPassword());//密码输出为null
System.out.println(user.getAuthorities());
//重定向到百度。这只是一个示例,具体需要看项目业务需求
httpServletResponse.sendRedirect("http://www.baidu.com");
}
}
1.此时可以修改配置项,及修改成功跳转的页面:
// 表单认证
http.formLogin()
.loginProcessingUrl("/login") //当发现/login时认为是登录,需要执行UserDetailsServiceImpl
.successHandler(new MyAuthenticationSuccessHandler())
//.successForwardUrl("/toMain") //此处是post请求
.failureForwardUrl("/fail") //登录失败跳转地址
.loginPage("/login.html");
2.登录失败跳转
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
操作失败,请重新登录
</body>
</html>
// 表单认证
http.formLogin()
.loginProcessingUrl("/abc") //当发现/login时认为是登录,需要执行 UserDetailsServiceImpl
.successForwardUrl("/toMain") //此处是post请求
.failureForwardUrl("/fail") //登录失败跳转地址
.loginPage("/login");
添加控制器方法
在控制器类中添加控制器方法,方法映射路径/fail。此处要注意:由于是POST请求访问/fail。所以如果返回值直接转发到fail.html中,及时有效果,控制台也会报警告,提示fail.html不支持POST访问方式。
@PostMapping("/fail")
public String fail(){
return "redirect:/fail.html";
}
3.自定义登录失败处理器
public class MyForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
httpServletResponse.sendRedirect("/fail.html");
}
}
//在配置类中
// 表单认证
http.formLogin()
.loginProcessingUrl("/login") //当发现/login时认为是登录,需要执行UserDetailsServiceImpl
.successHandler(new MyAuthenticationSuccessHandler())
//.successForwardUrl("/toMain") //此处是post请求
.failureHandler(new MyForwardAuthenticationFailureHandler())
//.failureForwardUrl("/fail") //登录失败跳转地址
.loginPage("/login.html");
Remember Me
1.导入
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置数据源
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
2.在客户端登录页面中添加remember-me的复选框,只要用户勾选了复选框下次就不需要进行登录了。
<form action = "/login" method="post">
用户名:<input type="text" name="username"/><br/>
密码:<input type="text" name="password"/><br/>
<input type="checkbox" name="remember-me" value="true"/> <br/>
<input type="submit" value="登录"/>
</form>
3.配置PersistentTokenRepository
//第一次启动会创建一张persistent_logins的表存储登录信息,和Cookie对应
@Bean
public PersistentTokenRepository getPersistentTokenRepository() {
JdbcTokenRepositoryImpl jdbcTokenRepositoryImpl=new JdbcTokenRepositoryImpl();
jdbcTokenRepositoryImpl.setDataSource(dataSource);
// 自动建表,第一次启动时需要,第二次启动时注释掉
// jdbcTokenRepositoryImpl.setCreateTableOnStartup(true);
return jdbcTokenRepositoryImpl;
}
4.修改SecurityConfig
// remember Me
http.rememberMe()
.userDetailsService(userDetailsService) // 登录逻辑交给哪个对象
.tokenRepository(repository); // 持久层对象
此时基础已经配置完了访问页面,关闭浏览器,在次访问可以不用登录,Cookie的默认有效时间为两周
注意:
Cookie的功能是保证下次不用登录,而不是保证本次
Session是保证本次登录的.
5.整体配置类及其他配置:
@SpringBootConfiguration
public class Myconfig {
@Autowired
private DataSource dataSource;
@Autowired
private AdminServiceImpl adminService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity,PersistentTokenRepository pt) throws Exception {
//登录及表单提交配置
httpSecurity.formLogin()
.usernameParameter("user")//默认username,就是表单中input中的name属性值
.passwordParameter("pwd")//默认password,就是表单中input中password的name值
.loginPage("/login")//表单页面
.loginProcessingUrl("/login")//(默认/login)表单提价对应控制器执行,即form表单active的地址
.defaultSuccessUrl("/success")//重定向指定登录成功后的跳转页面get
// .successForwardUrl("/success")//转发指定登录成功后的跳转页面//只能用post
;
httpSecurity.rememberMe()
.rememberMeCookieName("rm")//Cookie名称
.tokenValiditySeconds(100)//Cookie存活时间
.rememberMeParameter("rm")//和form表单中记住我input中的name的值对应,两者要一致
//.rememberMeCookieDomain("/")//设置Cookie访问的域值,没有就不要写
.tokenRepository(pt)//持久层对象
.userDetailsService(adminService);//登录逻辑交给哪个对象
//拦截url
httpSecurity.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
//关闭csrf防护
httpSecurity.csrf().disable();
return httpSecurity.build();
}
@Bean
public PersistentTokenRepository getTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
// jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
//配置密码解析器
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
退出登录
用户只需要向Spring Security项目中发送/logout退出请求即可。
登录成功后在success页面添加
<a href="/lgo">退出</a>
默认是/logout
修改SecurityConfig
//配置登出页面
httpSecurity.logout()
.logoutUrl("/lgo")//(默认是/logout)和登出标签的href中的值相等
.logoutSuccessUrl("/main")//退出成功后跳往的页面
//.addLogoutHandler()//自定义登出逻辑,参数为实现LogoutHandler接口并重写方法的实例
.invalidateHttpSession(true);//设置注销session 默认是true
//拦截url
httpSecurity.authorizeRequests()
.antMatchers("/login","/main").permitAll()//记得在这里添加非拦截url:/main
.anyRequest().authenticated();
注意:
退出时: 默认会销毁session和Cookie(remember-me)
授权
授权组成两部分:路径匹配 + 执行授权操作
第一部分:路径匹配
//拦截url
httpSecurity.authorizeRequests()
.antMatchers("/login","/main").permitAll()//不认证直接放心的url
.anyRequest().authenticated();//所有类容进行认证
antMatchers(“”):支持ant表达式 ?:匹配一个字符 :匹配0个或多个字符 :0个或多个目录 HttpMethod.GET:可以定义请求方式
/a? 例如:/aa /ab /ac(都可以) /abc(不可以)
/a 例如:/a /ab /acd(都可以) /a/b(不可以)
/a/ 例如:/a /a/c /a/c/d(都可以)
anyRequest() :所有的地址
regexMatchers(“.+[.]do”):支持正则表达式 里面直接定义正则表达式即可 HttpMethod.GET:可以定义请求方式
第二部分:执行授权操作:内置权限,自定义权限
内置的权限
permitAll():表示所匹配的URL任何人都允许访问
authenticated():表示所匹配的URL都需要被认证才能访问 :表单提交认证+rememberMe()
anonymous():表示可以匿名访问匹配的URL 必须不用认证 如果已经认证 无法访问 :登录页面需要指定当前权限
denyAll():表示所匹配的URL都不允许被访问。
rememberMe():被“remember me”的用户允许访问
fullyAuthenticated():如果用户不是被remember me的,才可以访问。
自定义权限: 方法 + 注解
方法:
需求:有两个角色 分别为: 管理员和普通用户
有多个权限 分别为: flower:save flower:edit flower:findAll flower:remove
管理员具有4个权限 普通用户 只有 添加 和查询权限
使用方法自定义权限:
1.配置实现UserDetailsService的实现类重写的loadUserByUsername方法
将该用户所有对应的角色/权限告诉到SpringSecurity
@Service
public class MyUserDetailsServiceImpl implements UserDetailsService {
// 如果觉得编译错误闹心,在Mapper上添加@Component即可。
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("此方法被执行");
User user = userMapper.selectByUsername(username);
if(user==null){
throw new UsernameNotFoundException("用户名不存在");
}
// 查询用户对应的权限
List<String> listPermission = userMapper.selectPermissionByUsername(username);
//将数据表中查出来的List类型数据转换为SimpleGrantedAuthority类型即可
List<SimpleGrantedAuthority> listAuthority = new ArrayList<SimpleGrantedAuthority>();
for(String permisssion : listPermission){
listAuthority.add(new SimpleGrantedAuthority(permisssion));
}
return new org.springframework.security.core.userdetails.User(username,user.getPassword(),listAuthority);
}
}
2.判定
根据访问用户权限判定:
hasAuthority():判断当前用户是否具有指定权限
hasAnyAuthority():判断当前用户是否具有指定权限之一
根据访问用户角色判定:
hasRole():判断当前用户是否具有指定角色
hasAnyRole():判断当前用户是否具有指定角色之一
其他:
hasIpAddress():判断当前访问地址是否是指定的IP
access(): 自定义授权整个流程
例:
//对URL地址进行拦截
http.authorizeRequests()
.antMatchers("/MyLogin").permitAll() //指定的路径全部放行
/*.antMatchers("/save").hasAuthority("flower:save")
.antMatchers("/findAll").hasAuthority("flower:findAll")
.antMatchers("/edit").hasAuthority("flower:edit")
.antMatchers("/remove").hasAuthority("flower:remove")*/
/*.antMatchers("/save","/findAll").hasAnyRole("普通用户","管理员")
.antMatchers("/edit","/remove").hasRole("管理员")*/
//.antMatchers("/save").hasIpAddress("127.0.0.1")
//request authentication 这两个对象已经创建 直接传递即可
//.antMatchers("/save").access("@myServiceImpl.isMenuPermission(request,authentication)")//
.anyRequest().authenticated();//所有的请求都需要认证
==================================================================================================================================================================================================================
以上的登录用户权限判断实际上底层实现都是调用access(表达式)
3.可以自定义权限校验: 如某个访问路径要多个权限才能访问,但是security给的都是过关系,所以需要自定义
接口名称任意、方法返回值固定、方法名称任意、参数固定
public interface MyService {
boolean isMenuPermission(HttpServletRequest request, Authentication authentication);
}
业务层实现
@Service
public class MyServiceImpl implements MyService {
@Override
public boolean isMenuPermission(HttpServletRequest request, Authentication authentication) {
Object obj = authentication.getPrincipal();
if(obj instanceof UserDetails) {
UserDetails userDetails = (UserDetails) obj;
//获取当前认证成功用户的权限,就是之前数据表中查出来的权限
Collection<? extends GrantedAuthority> list = userDetails.getAuthorities();
//自己进行判断即可
boolean b1 = list.contains(new SimpleGrantedAuthority("savc"));
boolean b2 = list.contains(new SimpleGrantedAuthority("数据库权限"));
return b1&&b2;
}
return false;
}
}
配置: SecurityConfig
// request,authentication 这两个参数不需要改自动进行传递 @bean对象名称+方法名称
.antMatchers("/bjsxt").access("@myServiceImpl.isMenuPermission(request,authentication)")
注解:
判断角色或权限
@PreAuthorize/@PostAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true)
区别:@PreAuthorize如果没有权限 就不会进入控制单元方法执行
@PostAuthorize 如果没有权限 也会进入控制单元方法执行
判断角色
@EnableGlobalMethodSecurity(securedEnabled = true)
@Secured:是专门用于判断是否具有角色的。能写在方法或类上。@Secured参数要以ROLE_开头。
在控制器方法上添加@PreAuthorize,参数可以是任何access()支持的表达式。
如果用户没有管理员角色,不会打印preAuthorize
@RequestMapping("/preAuthorize")
@ResponseBody
@PreAuthorize("hasRole('ROLE_管理员')")
public String preAuthorize(){
System.out.println("preAuthorize");
return "preAuthorize";
}
如果用户没有管理员角色也会打印PostAuthorize
@RequestMapping("/postAuthorize")
@ResponseBody
@PostAuthorize("hasRole('ROLE_管理员')")
public String postAuthorize(){
System.out.println("PostAuthorize");
return "preAuthorize";
}
无权限访问跳转页面
1.创建控制层实现AccessDeniedHandler接口
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"权限不足,请联系管理员!\"}");
out.flush();
}
}
2.修改配置类
@Autowired
private AccessDeniedHandler accessDeniedHandler;
//异常处理
http.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
Thymeleaf中Spring Security的使用
导入依赖
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
在html页面中引入thymeleaf命名空间和security命名空间
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
根据源码得出下面属性:
- name:登录账号名称
- principal:登录主体,在自定义登录逻辑中是UserDetails
- credentials:凭证
- authorities:权限和角色
- details:实际上是WebAuthenticationDetails的实例。可以获取remoteAddress(客户端ip)和sessionId(当前sessionId)
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
success
<a href="/save" sec:authorize="hasAuthority('save')">新增</a>
<a href="/edit" sec:authorize="hasAuthority('edit')">修改</a>
<a href="/find" sec:authorize="hasAuthority('find')">查看</a>
</body>
</html>
获取信息
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
登录账号:<span sec:authentication="name">123</span><br/>
登录账号:<span sec:authentication="principal.username">456</span><br/>
凭证:<span sec:authentication="credentials">456</span><br/>
权限和角色:<span sec:authentication="authorities">456</span><br/>
客户端地址:<span sec:authentication="details.remoteAddress">456</span><br/>
sessionId:<span sec:authentication="details.sessionId">456</span><br/>
通过权限判断:
<button sec:authorize="hasAuthority('/insert')">新增</button>
<button sec:authorize="hasAuthority('/delete')">删除</button>
<button sec:authorize="hasAuthority('/update')">修改</button>
<button sec:authorize="hasAuthority('/select')">查看</button>
<br/>
通过角色判断:
<button sec:authorize="hasRole('abc')">新增</button>
<button sec:authorize="hasRole('abc')">删除</button>
<button sec:authorize="hasRole('abc')">修改</button>
<button sec:authorize="hasRole('abc')">查看</button>
</body>
</html>
编写控制器进行转发
@RestController
public class EController {
@PreAuthorize("hasAuthority('save')")
@RequestMapping("/save")
public String save(){
return "save";
}
@PreAuthorize("hasAuthority('edit')")
@RequestMapping("/edit")
public String edit(){
return "edit";
}
@PreAuthorize("hasAuthority('find')")
@RequestMapping("/find")
public String find(){
return "find";
}
}
Spring Security中CSRF
默认开启
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack” 或者Session Riding。通过伪造用户请求访问受信任站点的非法请求访问。
跨域:只要网络协议,ip地址,端口中任何一个不相同就是跨域请求。
客户端与服务进行交互时,由于http协议本身是无状态协议,所以引入了cookie进行记录客户端身份。在cookie中会存放session id用来识别客户端身份的。在跨域的情况下,session id可能被第三方恶意劫持,通过这个session id向服务端发起请求时,服务端会认为这个请求是合法的,可能发生很多意想不到的事情。
CSRF攻击攻击原理及过程如下:
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
- 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
- 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
Spring Security中CSRF
从Spring Security4开始CSRF防护默认开启。默认会拦截请求。进行CSRF处理。CSRF为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务端产生)的内容,如果token和服务端的token匹配成功,则正常访问。
6. 当服务器加载登录页面。(loginPage中的值,默认/login),先生成csrf对象,并放入作用域中,key为_csrf。之后会对${_csrf.token}进行替换,替换成服务器生成的token字符串。
7. 用户在提交登录表单时,会携带csrf的token。如果客户端的token和服务器的token匹配说明是自己的客户端,否则无法继续执行。
8. 用户退出的时候,必须发起POST请求,且和登录时一样,携带csrf的令牌。
在表单中添加下面一行即可:
<input type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}"/>
在配置类中注释掉CSRF防护失效
// 关闭csrf防护
// http.csrf().disable();