解决系统安全问题的框架。如果没有安全框架,我们需要手动处理每个资源的访问控制。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.5version>
parent>
<groupId>org.examplegroupId>
<artifactId>SpringSecuritytestartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
准备一个Controller
package com.yjx23332.security.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class LoginController {
@GetMapping("/test")
public String login(){
log.info("执行登陆");
return "已登录";
}
}
我们启动项目访问,会发现需要用户名以及密码

这是SpringSecurity生效后的页面,
默认
账户:user
密码:在控制台可以看到

随后才会跳转至

package org.springframework.security.core.userdetails;
public interface UserDetailsService {
/**
* @param 前端传入用户名称
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetails
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.security.core.userdetails;
import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
public interface UserDetails extends Serializable {
//返回用户权限,且不能返回空
/**
* Returns the authorities granted to the user. Cannot return null.
* @return the authorities, sorted by natural key (never null)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* Returns the password used to authenticate the user.
* @return the password
*/
String getPassword();
/**
* Returns the username used to authenticate the user. Cannot return
* null.
* @return the username (never null)
*/
String getUsername();
//用户是否过期
/**
* Indicates whether the user's account has expired. An expired account cannot be
* authenticated.
* @return true if the user's account is valid (ie non-expired),
* false if no longer valid (ie expired)
*/
boolean isAccountNonExpired();
//用户是否锁定
/**
* Indicates whether the user is locked or unlocked. A locked user cannot be
* authenticated.
* @return true if the user is not locked, false otherwise
*/
boolean isAccountNonLocked();
//凭证是否过期
/**
* Indicates whether the user's credentials (password) has expired. Expired
* credentials prevent authentication.
* @return true if the user's credentials are valid (ie non-expired),
* false if no longer valid (ie expired)
*/
boolean isCredentialsNonExpired();
//是否可用
/**
* Indicates whether the user is enabled or disabled. A disabled user cannot be
* authenticated.
* @return true if the user is enabled, false otherwise
*/
boolean isEnabled();
}
它的实现类是User,注意不要和我们自己的混淆
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
如果我们想要用自己的账号密码等,我们需要覆盖passwordEncoder接口的实现
/*
* Copyright 2011-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.crypto.password;
/**
* Service interface for encoding passwords.
*
* The preferred implementation is {@code BCryptPasswordEncoder}.
*
* @author Keith Donald
*/
public interface PasswordEncoder {
// 加密密码,返回加密后字符串
/**
* Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
* greater hash combined with an 8-byte or greater randomly generated salt.
*/
String encode(CharSequence rawPassword);
//匹配 原始密码(也即是传入的密码) 加密码(数据库读出的)
/**
* Verify the encoded password obtained from storage matches the submitted raw
* password after it too is encoded. Returns true if the passwords match, false if
* they do not. The stored password itself is never decoded.
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
//是否可以被二次加密,默认返回false
/**
* Returns true if the encoded password should be encoded again for better security,
* else false. The default implementation always returns false.
* @param encodedPassword the encoded password to check
* @return true if the encoded password should be encoded again for better security,
* else false.
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
官方推荐使用BCryptpassword

创建Config包,设置自动装配的加密类
package com.yjx23332.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
接下来的数据操作均为模拟,我们自己实现UserDetailsService
为了避免循环依赖,此处我们用懒加载
package com.yjx23332.security.service.Impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Lazy
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1. 查询数据库判断用户名是否存在,不存在则抛出UsernameNotFoundException异常
if(!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
//2. 把查询出来的密码(注册时已经加密过得密码)与传入密码比较。此处就不查了,直接赋值
String password = passwordEncoder.encode("123");
//使用的是Security的User
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}
这里放密码是数据库的密码,之后会自动比较,我们不需要关注。
修改config
package com.yjx23332.security.config;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
//表单提交
/**
* /loginPage 这里是一个 controller 由它跳转至登陆页面,所有的未登录用户都会被跳转至这里
* 登陆页面的表单
httpSecurity.formLogin()
//当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl
.loginProcessingUrl("/login")
.loginPage("/loginPage")//自定义登陆页面
//登陆成功跳转页面,必须是Post请求
.successForwardUrl("/success")
//登陆失败
.failureForwardUrl("/error");
//授权认证
httpSecurity.authorizeRequests()
//进入login.html、error.html不需要被认证
.antMatchers("/loginPage","/error").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().authenticated();
//关闭csrf防护
httpSecurity.csrf().disable();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
表单参数必须为 username 和 password 两个参数,以为它使用了这两个常量

如果我们不想用它自定义的,
httpSecurity.formLogin()
//设置参数别名
.usernameParameter("username")
.passwordParameter("password")
//当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl
.loginProcessingUrl("/login")
.loginPage("/loginPage")//自定义登陆页面
//登陆成功跳转页面,必须是Post请求
.successForwardUrl("/success")
//登陆失败
.failureForwardUrl("/error");
如果我们想要跳转至其他网站,我们可以需要先注释掉相关配置,在配置新的处理器
还需要重写AuthenticationSuccessHandler与AuthenticationFailureHandler
AuthenticationSuccessHandler
httpSecurity.formLogin()
//设置参数别名
.usernameParameter("username")
.passwordParameter("password")
//当发现是/login时,认为是登陆,必须和表单提交地址一样,执行UserDetailServiceImpl
.loginProcessingUrl("/login")
.loginPage("/loginPage")//自定义登陆页面
//登陆成功跳转页面,必须是Post请求
.successHandler(new MyAuthenticationHandler("http://www.baidu.com"));
//.successForwardUrl("/success")
//登陆失败
//.failureForwardUrl("/error");
重写该方法
package com.yjx23332.security.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyAuthenticationHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationHandler(String url){
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// User user = (User)authentication.getPrincipal();
// 安全原因,该方式不会获取到密码,是Null
// user.getPassword();
response.sendRedirect(url);
}
}
AuthenticationFailureHandler
package com.yjx23332.springsecurity.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyAuthenticationFailurehandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"login failed\"}");
writer.flush();
writer.close();
}
}
用于所有请求的统一设置,放在最后起到类似else或者default的作用,对前面没有匹配到的进行处理。
将最不具体的路径(如anyRequest())放在最后面。如果不这样做,那不具体的路径配置将会覆盖掉更为具体的路径配置。
.antMatchers(HttpMethod.GET,"").permitAll()
.antMatchers(HttpMethod.POST,"").denyAll()
regex匹配格式,可以参考
regex 正则表达式
.regexMatchers(HttpMethod.DELETE,"^delete").permitAll()
.regexMatchers(HttpMethod.POST,"").denyAll()
针对于全局前缀
比如我们配置如下
spring:
mvc:
servlet:
path: /security
随后我们访问时就需要
localhost:8080/security/test
针对这个,我们需要配置
.mvcMatchers("/login").servletPath("/security").permitAll()
当然我们也可以
.antMatchers(HttpMethod.GET,"/security/login").permitAll()
static final String permitAll = "permitAll";
private static final String denyAll = "denyAll";
//可以匿名访问,会进入到拦截链中
private static final String anonymous = "anonymous";
//认证
private static final String authenticated = "authenticated";
//完全认证
private static final String fullyAuthenticated = "fullyAuthenticated";
//记住我,勾选了记住我的用户可访问
private static final String rememberMe = "rememberMe";
决定了权限,严格区分大小写
.antMatchers("").hasAuthority("admin")
//指定多个,只要有一个就可访问
.antMatchers("").hasAnyAuthority()
角色格式:
ROLE_XXX
new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_abc"));
.antMatchers("/login").hasRole("abc")
.antMatchers("/login").hasAnyRole("abc")
也是严格区分大小写
获取
request.getRemoteAddr();
package com.yjx23332.security.handler;
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.io.PrintWriter;
@Component
public class MyAccessDeniedhandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"access denied\"}");
writer.flush();
writer.close();
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessDeniedhandler myAccessDeniedhandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
//表单提交
httpSecurity.formLogin()
.loginPage("loginPage")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailurehandler());
//授权认证
httpSecurity.authorizeRequests()
.antMatchers("/loginPage").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().authenticated();
//关闭csrf防护
httpSecurity.csrf().disable();
//异常处理
httpSecurity.exceptionHandling()
.accessDeniedHandler(myAccessDeniedhandler);
}
}
AuthenticationService 是自己创建的接口。
package com.yjx23332.security.service.Impl;
import com.yjx23332.security.service.AuthenticationService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.servlet.http.HttpServletRequest;
import java.util.Collection;
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
@Override
public boolean hasPermision(HttpServletRequest request, Authentication authentication) {
Object object = authentication.getPrincipal();
if(object instanceof UserDetails){
UserDetails userDetails = (UserDetails) object;
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
//查看是否含有该URI的权限
return authorities.contains(new SimpleGrantedAuthority(request.getRequestURI()));
}
return false;
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessDeniedhandler myAccessDeniedhandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
//表单提交
httpSecurity.formLogin()
.loginPage("loginPage")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailurehandler());
//授权认证
httpSecurity.authorizeRequests()
.antMatchers("/loginPage").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().access("@authenticationServiceImpl.hasPermision(HttpServletRequest request, Authentication authentication)");
//关闭csrf防护
httpSecurity.csrf().disable();
//异常处理
httpSecurity.exceptionHandling()
.accessDeniedHandler(myAccessDeniedhandler);
}
}
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessDeniedhandler myAccessDeniedhandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
//表单提交
httpSecurity.formLogin()
.loginPage("loginPage")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailurehandler());
//授权认证
httpSecurity.authorizeRequests()
.antMatchers("/loginPage").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
HttpServletRequest request = requestAuthorizationContext.getRequest();
//查看是否含有该URI的权限,有则返回true
return new AuthorizationDecision(
authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())
)
);
});
//关闭csrf防护
httpSecurity.csrf().disable();
//异常处理
httpSecurity.exceptionHandling()
.accessDeniedHandler(myAccessDeniedhandler);
}
}
判断是否具有角色。能写在方法或者类上,Controller和Service上,参数仍然要以ROLE_开头。严格区分大小写。
默认不可用,需要
package com.yjx23332.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class MainApplication {
public static void main(String[] args){
SpringApplication.run(MainApplication.class,args);
}
}
配置类中的角色全部注释掉
@Secured("ROLE_abc")
@RequestMapping("test")
public String toMain(){ return "test" }
方法执行前判断与方法执行后判断。写在方法或者类上
同理也要开启
package com.yjx23332.security;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MainApplication {
public static void main(String[] args){
SpringApplication.run(MainApplication.class,args);
}
}
写法与旧版access一致,但是此处也可以用ROLE_开头。严格区分大小写。
@PreAuthorize("hasRole('abc')")
@RequestMapping("test")
public String toMain(){ return "test" }
用户只需要在登陆时添加remember-me复选框,取值为true。SpringSecurity会自动把用户存储到数据院中,以后就可以不登陆进行访问。
需要配置mybatis,此处省略。
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessDeniedhandler myAccessDeniedhandler;
@Autowired
UserDetailsService userDetailsService;
@Autowired
DataSource dataSource;
@Autowired
PersistentTokenRepository persistentTokenRepository;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
//表单提交
httpSecurity.formLogin()
.loginPage("loginPage")
.loginProcessingUrl("/login")
.successHandler(new MyAuthenticationSuccessHandler())
.failureHandler(new MyAuthenticationFailurehandler());
//授权认证
httpSecurity.authorizeHttpRequests()
.antMatchers("/loginPage").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().access((authenticationSupplier, requestAuthorizationContext) -> {
Collection<? extends GrantedAuthority> authorities = authenticationSupplier.get().getAuthorities();
HttpServletRequest request = requestAuthorizationContext.getRequest();
//查看是否含有该URI的权限,有则返回true
return new AuthorizationDecision(
authorities.contains(new SimpleGrantedAuthority(request.getRequestURI())
)
);
});
//关闭csrf防护
httpSecurity.csrf().disable();
//记住我
httpSecurity.rememberMe()
//设置失效时间,默认2周
.tokenValiditySeconds(60)
//修改remember-me对应的参数名
.rememberMeParameter("")
//domain域
.rememberMeCookieDomain("")
//Cookie名字
.rememberMeCookieName("")
//自定义登陆逻辑
.userDetailsService(userDetailsService)
//持久层对象
.tokenRepository(persistentTokenRepository);
//异常处理
httpSecurity.exceptionHandling()
.accessDeniedHandler(myAccessDeniedhandler);
}
@Bean
public PasswordEncoder getPasswordEncoder(){ return new BCryptPasswordEncoder(); }
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
//自动建表,第一次启动时需要,第二次启动需要注释掉
jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
}
直接访问即可
/logout
也可以,设置退出登录的位置等信息
httpSecurity.logout()
//设置前端退出的URI
.logoutUrl("/logout")
//设置退出跳转页面
.logoutSuccessUrl("");
CSRF(Cross-site request forgery)跨站请求伪造,通过伪造用户请求访问收信人站点的非法请求访问。
不法分子通过劫持Cookie中SessionID来让服务器认为对方就是客户。
在跨域的情况下,sessionID可能被恶意劫持。
SpringSecurity4开始默认开启,会默认拦截请求,进行CSRF处理。为了保证不是其他第三方网站访问,要求访问时携带参数名为_csrf值为token(token在服务器产生)的内容,如果token和服务器token匹配成功,则正常访问。
我们之前一致都是关闭状态,开启后我们就需要增加一个获取服务端token的过程。
我们以thymeleaf为例。
前端需要先获取一个服务器生成码,随后再发送。增加如下内容即可。
DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Titletitle>
head>
<body>
<form action="/login" method="post">
<intput type="hidden" th:value="${_csrf.token}" name="_csrf" th:if="${_csrf}">
用户名:<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>
body>
html>
我们将上述的过程整合一下,如下完成前后端模式
package com.yjx23332.security.config;
import com.yjx23332.security.handler.*;
import com.yjx23332.security.service.Impl.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
/**
* 绑定新的认证处理方法
* */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//表单提交
httpSecurity.formLogin()
.permitAll()
.successHandler(new MyAuthenticationSuccessHandler("https://baidu.com"))
.failureHandler(new MyAuthenticationFailurehandler());
//授权认证
httpSecurity.authorizeRequests()
//所有的请求都必须登陆之后,被访问
.anyRequest().authenticated();
//异常处理
httpSecurity.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedhandler())
.authenticationEntryPoint(new MyAuthenticationEntryPoint());
//登出处理
httpSecurity.logout()
.permitAll()
.logoutSuccessHandler(new MyLogoutSuccessHandler())
.deleteCookies();
//限制账户登录数目
httpSecurity.sessionManagement().
maximumSessions(1)
.expiredSessionStrategy(new MySessionInformationExpiredStrategy());
//csrf防御关闭
httpSecurity.csrf().disable();
//允许跨域
httpSecurity.cors().disable();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
package com.yjx23332.springsecurity.handler;
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.io.PrintWriter;
public class MyAccessDeniedhandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"access denied\"}");
writer.flush();
writer.close();
}
}
package com.yjx23332.springsecurity.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, IOException {
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setContentType("text/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"annoymous refused\"}");
writer.flush();
writer.close();
}
}
package com.yjx23332.springsecurity.handler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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.io.PrintWriter;
public class MyAuthenticationFailurehandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write("{\"status\":\"error\",\"msg\":"+exception.getStackTrace()+"}");
writer.flush();
writer.close();
}
}
package com.yjx23332.springsecurity.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private String url;
public MyAuthenticationSuccessHandler(String url){
this.url = url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User user = (User)authentication.getPrincipal();
// 安全原因,该方式不会获取到密码,是Null
log.info("密码:{}",user.getPassword());
log.info("账户:{}",user.getUsername());
log.info("权限:{}",user.getAuthorities());
response.sendRedirect(url);
}
}
package com.yjx23332.springsecurity.handler;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
httpServletResponse.setStatus(HttpServletResponse.SC_OK);
httpServletResponse.setHeader("Content-Type","application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"logout success\"}");
writer.flush();
writer.close();
}
}
package com.yjx23332.springsecurity.handler;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException {
HttpServletResponse httpServletResponse = sessionInformationExpiredEvent.getResponse();
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
httpServletResponse.setHeader("Content-type","application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write("{\"status\":\"error\",\"msg\":\"connection invalid\"}");
writer.flush();
writer.close();
}
}
package com.yjx23332.security.service.Impl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Lazy
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1. 查询数据库判断用户名是否存在,不存在则抛出UsernameNotFoundException异常
if(!"admin".equals(username)){
throw new UsernameNotFoundException("用户名不存在");
}
//2. 把查询出来的密码(注册时已经加密过得密码)与传入密码比较。此处就不查了,直接赋值
String password = passwordEncoder.encode("123");
//使用的是Security的User
return new User(username,password, AuthorityUtils.commaSeparatedStringToAuthorityList("admin,normal"));
}
}

图片源自互联网





我们登陆的时候可以用微信、QQ的方式进行一键登陆,并且不存在则进行绑定。
不同网站代码、认证标准不同。因此Oauth2主要解决的就是通用标准的问题,实现跨站系统认证,各系统之间要遵循一定的接口协议。
优点:
缺点:
授权服务器
图片来源自互联网

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.6.5version>
parent>
<groupId>org.examplegroupId>
<artifactId>SpringSecuritytestartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<maven.compiler.source>11maven.compiler.source>
<maven.compiler.target>11maven.compiler.target>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<java.version>11.0.2java.version>
<spring-cloud.version>2021.1spring-cloud.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-securityartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-oauth2artifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.securitygroupId>
<artifactId>spring-security-testartifactId>
<scope>testscope>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
使用自定义的User,相关部分替换即可
package com.yjx23332.security.entity.dto;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class User implements UserDetails {
private String username;
private String password;
private List<GrantedAuthority> authorityList;
public User(String username,String password,List<GrantedAuthority> authorities){
this.username = username;
this.password = password;
this.authorityList = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
认证服务器配置
package com.yjx23332.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@Configuration
@EnableAuthorizationServer
/**
* 授权服务器
* */
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置Client-ID
.withClient("admin")
//配置Client-secret
.secret(passwordEncoder.encode("112233"))
//访问token有效期
.accessTokenValiditySeconds(3600)
//授权成功后跳转
.redirectUris("http://www.baidu.com")
//使用范围
.scopes("all")
//授权码模式
.authorizedGrantTypes("authorization_code");
}
}
资源服务器配置
package com.yjx23332.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
* 资源服务器配置
* */
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//所有资源都需要被认证
.anyRequest()
.authenticated()
.and()
//有权限的用户可访问
.requestMatchers()
.antMatchers("/user/**");
}
}
security配置
package com.yjx23332.security.config;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//表单提交
httpSecurity.formLogin()
.permitAll();
//授权认证
httpSecurity.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().authenticated();
//登出处理
httpSecurity.logout()
.permitAll();
//csrf防御关闭
httpSecurity.csrf().disable();
//允许跨域
httpSecurity.cors().disable();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
controller配置
资源服务接口
package com.yjx23332.security.controller;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 获取当前用户
* */
@RequestMapping("/getCurrentUser")
public Object getCurrentUser(Authentication authentication){
return authentication.getPrincipal();
}
}
访问页面为
localhost:8080/oauth/authorize?response_type=code&client_id=admin&redirect_url=http://www.baidu.com&scope=all
登陆后跳转至如下默认页面

上方就为授权码

接下来用Post的方式,去获取授信息
笔者此处使用PostMan
首先我们要填写如下信息,右侧username和password是客户端的内容,我们已经在配置校验的时候已经配好了

我们接下俩配置如下内容后(也与我们配置的校验服务器一致),发送

通过令牌获取

如果想要替换授权码页面怎么做?可以参考自定义OAuth2授权同意页面
package com.yjx23332.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
@Configuration
@EnableAuthorizationServer
/**
* 授权服务器
* */
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置Client-ID
.withClient("admin")
//配置Client-secret
.secret(passwordEncoder.encode("112233"))
//访问token有效期
.accessTokenValiditySeconds(3600)
//授权成功后跳转
.redirectUris("http://www.baidu.com")
//使用范围
.scopes("all")
//授权码模式
.authorizedGrantTypes("password");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
package com.yjx23332.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
//表单提交
httpSecurity.formLogin()
.permitAll();
//授权认证
httpSecurity.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
//所有的请求都必须登陆之后,被访问
.anyRequest().authenticated();
//登出处理
httpSecurity.logout()
.permitAll();
//csrf防御关闭
httpSecurity.csrf().disable();
//允许跨域
httpSecurity.cors().disable();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
我们将获取令牌改为如下内容后发送,Authorization不用修改

