siki学院课程:http://www.sikiedu.com/course/366 (SpringSecurity和SpringSocial认证授权)
SpringSecurity可以理解为一系列的拦截器(filter)。将我们的服务保护起来,访问任何资源都需要身份认证。配置filter,SpringBoot已经帮我们默认配置好了。如果需要自定义配置,我们需要继承WebSecurityConfigurerAdapter类。
一、SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
二、自定义配置SpringSecurity
配置类继承WebSecurityConfigurerAdapter,重写configure(HttpSecurity http)方法。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
.and()
// 请求授权
.authorizeRequests()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
编写controller
@RestController
public class UserController {
@GetMapping("/user")
public String user(){
return "user";
}
}
启动项目
SpringSecurity默认生成了一个登录密码,会在控制台输出。用SpringSecurity自带的登录界面进行认证的时候,必须是这个密码。用户名默认是user。否则认证不成功。
访问/user请求会被SpringSecurity拦截,跳转到认证界面。
认证成功之后,成功访问。
三、SpringSecurity原理
四、SpringSecurity设置密码
实现接口UserDetailsService。这个接口是SpringSecurity默认处理登录的。
public interface UserDetailsService {
// ~ Methods
// ========================================================================================================
/**
* Locates the user based on the username. In the actual implementation, the search
* may possibly be case sensitive, or case insensitive depending on how the
* implementation instance is configured. In this case, the <code>UserDetails</code>
* object that comes back may have a username that is of a different case than what
* was actually requested..
*
* @param username the username identifying the user whose data is required.
*
* @return a fully populated user record (never <code>null</code>)
*
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
实现UserDetailsService接口
重写的方法的返回值是UserDetails接口,SpringSecurity有默认的实现User类。注意要导SpringSecurity的包。
/**
* 用SpringSecurity默认的登录系统
*/
@Service
public class UserServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 用户名、密码、权限
*/
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
重新发布项目,测试。密码是123456,用户名任意。发现报错:
发现原因是设置的密码是明文,SpringSecurity5要求必须要加密。
密码加密处理
SecurityConfig中配置加密方式
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
.and()
// 请求授权
.authorizeRequests()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
service中加密
/**
* 用SpringSecurity默认的登录系统
*/
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/**
* 用户名、密码、权限
*/
return new User(username, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
再次测试,登录成功。
五、SpringSecurity在数据库中查询用户
配置数据库连接信息和配置JPA
application.yaml
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
用户实体
参考SpringSecurity默认实现的User代码。只修改我们需要的部分。
@Entity
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
/** 用户是否没有失效 **/
@Transient
private boolean accountNonExpired;
/** 用户是否冻结 **/
@Transient
private boolean accountNonLocked;
/** 证明是否过期 **/
@Transient
private boolean credentialsNonExpired;
/** 判断是否删除 **/
@Transient
private boolean enabled;
@Transient
private Set<GrantedAuthority> authorities;
/**
* 给hibernate用的构造方法
*/
public User() {
}
public User(Long id, String username, String password){
this.id = id;
this.username = username;
this.password = password;
}
/**
* 给SpringSecurity用的构造方法
*/
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) {
if (((username == null) || "".equals(username)) || (password == null)) {
throw new IllegalArgumentException(
"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));
}
private static SortedSet<GrantedAuthority> sortAuthorities(
Collection<? extends GrantedAuthority> authorities) {
Assert.notNull(authorities, "Cannot pass a null GrantedAuthority collection");
// Ensure array iteration order is predictable (as per
// UserDetails.getAuthorities() contract and SEC-717)
SortedSet<GrantedAuthority> sortedAuthorities = new TreeSet<>(
new AuthorityComparator());
for (GrantedAuthority grantedAuthority : authorities) {
Assert.notNull(grantedAuthority,
"GrantedAuthority list cannot contain any null elements");
sortedAuthorities.add(grantedAuthority);
}
return sortedAuthorities;
}
private static class AuthorityComparator implements Comparator<GrantedAuthority>,
Serializable {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
@Override
public int compare(GrantedAuthority g1, GrantedAuthority g2) {
// Neither should ever be null as each entry is checked before adding it to
// the set.
// If the authority is null, it is a custom authority and should precede
// others.
if (g2.getAuthority() == null) {
return -1;
}
if (g1.getAuthority() == null) {
return 1;
}
return g1.getAuthority().compareTo(g2.getAuthority());
}
}
/**
* 权限
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
/**
* 用户是否没有失效
*/
@Override
public boolean isAccountNonExpired() {
return accountNonExpired;
}
/**
* 用户是否冻结
*/
@Override
public boolean isAccountNonLocked() {
return accountNonLocked;
}
/**
* 证明是否过期
*/
@Override
public boolean isCredentialsNonExpired() {
return credentialsNonExpired;
}
/**
* 判断是否删除
*/
@Override
public boolean isEnabled() {
return enabled;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
}
UserServiceImpl修改为自定义的User实体
UserServiceImpl类
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/** 自己编写的用户实体类User **/
User user = userRepository.findUserByUsername(username);
if (user == null){
throw new UsernameNotFoundException(username);
}
/**
* 自己编写的用户实体类User
* 用户名、密码、权限
**/
return new User(username, passwordEncoder.encode(user.getPassword()), AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
UserRepository类
public interface UserRepository extends CrudRepository<User,Long> {
/**
* 根据用户名查找用户
* @param username
* @return
*/
@Query(value = "select * from user where username = ?1", nativeQuery = true)
User findUserByUsername(String username);
}
测试
运行之后,会在数据库中生成一张表。手动添加一条记录。
访问/user,会跳转到SpringSecurity的默认登录界面认证。输入数据库中的用户名和密码。发现认证成功。
总结
密码是SpringSecurity自动完成验证,我们只需要将正确的密码告诉SpringSecurity。我们通过用户名到数据库中查找用户,如果查到,就将查询到的用户密码告诉SpringSecurity。然后SpringSecurity就会将这个密码和默认登录界面接收到的密码对比,一样则认证成功,否则失败。
扩展
在UserServiceImpl中使用7个参数的构造方法。并修改enabled为false。
@Service
public class UserServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
/** 自己编写的用户实体类User **/
User user = userRepository.findUserByUsername(username);
if (user == null){
throw new UsernameNotFoundException(username);
}
/**
* 自己编写的用户实体类User
* 用户名、密码、权限
* 密码是SpringSecurity自动完成验证,我们只需要将正确的密码告诉SpringSecurity
**/
return new User(username, passwordEncoder.encode(user.getPassword()),false,true,true,true, AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN"));
}
}
测试,输入正确的用户名和密码。此时这4个布尔值会有不同的效果。
enabled = false
accountNonExpired = false
credentialsNonExpired = false
accountNonLocked = false
利用这4个字段,可以实现对账户的锁定、删除、冻结等等。
六、SpringSecurity自定义登录页面
配置自定义登录页面
login.html name必须为username和password。SpringSecurity底层就是使用的username和password。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<form action="/loginPage" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="text" name="password">
<button type="submit">登录</button>
</form>
</body>
</html>
SecurityConfig配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
.and()
// 请求授权
.authorizeRequests()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
测试
虽然跳转到了自定义的登录界面。但是报错了:
解决页面无限循环重定向
由于我们配置了所有请求都需要身份认证。我们自己配置的自定义登录页面,同时也是资源,不是SpringSecurity自带的界面,会被拦截。当我们访问请求/user时,首先会重定向到我们自己定义认证界面(login.html)。然而login.html页面还是需要认证,又重定向到认证界面(login.html)。出现了无限循环重定向。所以我们需要设置访问login.html页面不需要身份认证,这样才能使用我们自定义的登录界面来替换SpringSecurity默认的登录界面。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
访问登录页面成功,但是还不能成功认证,一直在登录页面。
完成自定义登录页面
SpringSecurity自带的处理用户名密码登录的filter:UsernamePasswordAuthenticationFilter
配置SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 登录页面的处理url
.loginProcessingUrl("/loginPage")
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated();
}
}
测试,登录还是没反应。
还需要关闭SpringSecurity自带的跨站请求伪造防护。
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/login.html")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
认证成功!
七、完成小需求
判断请求是否以html结尾,以html结尾,重定向到登录。不是以html结尾,需要身份认证。
SecurityConfig配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/require")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
SecurityController
@RestController
public class SecurityController {
/** 拿到引发跳转的之前的请求 **/
private RequestCache requestCache = new HttpSessionRequestCache();
/** 处理重定向 **/
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@GetMapping("/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public String require(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取引发跳转之前的请求
SavedRequest savedRequest = requestCache.getRequest(request, response);
// 判断之前的请求是否以html结尾
if (savedRequest != null){
// 引发跳转之前的请求
String url = savedRequest.getRedirectUrl();
// 以html结尾,重定向到登录
if (StringUtils.endsWithIgnoreCase(url,".html")){
redirectStrategy.sendRedirect(request,response,"/login.html");
}
}
// 不是以html结尾,需要身份认证
return "需要身份认证";
}
}
测试,首先访问/user:
访问/user.html,会跳转到登录页面。
输入正确的用户名和密码之后,由于没有定义/user.html。出现404。
八、自定义登录成功之后的Handler
SpringSecurity默认登录成功后会跳转到之前的请求。自定义登录成功需要实现AuthenticationSuccessHandler接口。
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Resource
private ObjectMapper objectMapper;
/**
* 登录成功之后调用的函数
* @param request request
* @param response response
* @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登陆成功=======>");
// 将authentication以json的形式返回页面
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
配置SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Resource
private LoginSuccessHandler loginSuccessHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/require")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
// 登录成功之后的处理handler
.successHandler(loginSuccessHandler)
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
测试访问/user.html。控制台输出:
登录成功之后的页面:
{
# 用户权限
"authorities": [
{
"authority": "ADMIN"
}
],
# 认证请求的信息
"details": {
"remoteAddress": "0:0:0:0:0:0:0:1",
"sessionId": "FEFB90B1D0D3681427301D8A0CC04D2B"
},
# 用户是否已经通过身份认证
"authenticated": true,
# UserDetails
"principal": {
"username": "哔哩哔哩",
"password": "$2a$10$E0wVWIV0hRdqA0BLZNwvTuT6MIRuAd8KCsNQXXtEi9xqJGiocH.JK",
"accountNonExpired": true,
"accountNonLocked": true,
"credentialsNonExpired": true,
"enabled": true,
"authorities": [
{
"authority": "ADMIN"
}
]
},
# 密码
"credentials": null,
# 用户名
"name": "哔哩哔哩"
}
九、自定义登录失败之后的Handler
SpringSecurity默认登录失败后还是跳转到登录界面。自定义登录失败需要实现AuthenticationFailureHandler接口。
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* 登录失败之后调用的方法
* @param request request
* @param response response
* @param exception 登陆失败产生的异常信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败=======>");
// 设置返回的状态码
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}
}
SecurityConfig配置
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/require")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
// 登录成功之后的处理handler
.successHandler(loginSuccessHandler)
// 登录成功之后的处理handler
.failureHandler(loginFailureHandler)
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
测试
访问/user.html,跳转到登录界面,随便输入用户名和密码:
结果:
十、自定义属性配置文件
Properties属性类
DemoSecurityProperties属性类
@ConfigurationProperties(prefix = "demo.security")
public class DemoSecurityProperties {
/** LoginType登录的方式,默认为JSON(restful风格) **/
private LoginType loginType = LoginType.JSON;
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
}
LoginType登录方式枚举
public enum LoginType {
JSON,
REDIRECT
}
自定义配置让其生效
DemoSecurityConfig配置类
@Configuration
@EnableConfigurationProperties(DemoSecurityProperties.class)
public class DemoSecurityConfig {
}
application.yaml
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
# 自定义配置属性
demo:
security:
login-type: json
登录成功handler中读取自定义的配置
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private DemoSecurityProperties demoSecurityProperties;
/**
* 登录成功之后调用的函数
* @param request request
* @param response response
* @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登陆成功=======>");
System.out.println(demoSecurityProperties.getLoginType());
// 将authentication以json的形式返回页面
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
}
测试,发现登录成功之后打印出了JSON。
修改application.yaml
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 13518529311
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
# 自定义配置属性
demo:
security:
login-type: redirect
测试发现登录成功之后打印出了REDIRECT。
现在我们可以通过application.yaml系统配置文件来更改我们自己自定义的属性,这样可以更为方便的提高软件通用性。只需要修改配置即可,不用修改代码。
提高软件通用性
登录成功handler
/**
* @Desc TODO
* SavedRequestAwareAuthenticationSuccessHandler为SpringSecurity默认处理成功的类
* @Author HeJin
* @Date 2021/3/5 9:32
*/
@Component
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{
@Autowired
private ObjectMapper objectMapper;
@Autowired
private DemoSecurityProperties demoSecurityProperties;
/**
* 登录成功之后调用的函数
* @param request request
* @param response response
* @param authentication 认证信息(发起的认证请求(ip、session)、认证成功之后的用户信息)
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登陆成功=======>");
// 自定义的处理方式
if (LoginType.JSON.equals(demoSecurityProperties.getLoginType())){
// 将authentication以json的形式返回页面
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
// 系统默认方式
else {
// 调用父类中的方法,跳转到之前的页面
super.onAuthenticationSuccess(request,response,authentication);
}
}
}
登录失败handler
/**
* @Desc TODO
* SimpleUrlAuthenticationFailureHandler 为SpringSecurity默认处理失败的类
* @Author HeJin
* @Date 2021/3/5 9:53
*/
@Component
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler{
@Autowired
private ObjectMapper objectMapper;
/** 自己的配置 **/
@Autowired
private DemoSecurityProperties demoSecurityProperties;
/**
* 登录失败之后调用的方法
* @param request request
* @param response response
* @param exception 登陆失败产生的异常信息
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败=======>");
// 自定义的处理方式
if (LoginType.JSON.equals(demoSecurityProperties.getLoginType())){
// 设置返回的状态码
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(exception));
}
// 系统默认方式
else {
super.onAuthenticationFailure(request,response,exception);
}
}
}
application.yaml配置为redirect时,使用SpringSecurity默认实现的方式。
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 13518529311
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
# 自定义配置属性
demo:
security:
login-type: redirect
登录成功后跳转到之前的页面:
登录失败:
application.yaml配置为json时,使用我们自己实现的,在页面打印json信息。
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 13518529311
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
# 自定义配置属性
demo:
security:
login-type: json
登录成功:
登录失败:
十一、SpringSecurity记住我的原理
十二、完成记住我功能
前端登录界面
记住我的name属性必须为remember-me。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<h1>登录</h1>
<form action="/loginPage" method="post">
<!--name必须为username-->
用户名:<input type="text" name="username">
<br>
<!--name必须为password-->
密码:<input type="text" name="password">
<br>
<!--name必须为remember-me-->
<input name="remember-me" type="checkbox" value="true">
记住我<br>
<button type="submit">登录</button>
</form>
</body>
</html>
配置SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private DataSource dataSource;
@Qualifier("userServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
};
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/require")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
// 登录成功之后的处理handler
.successHandler(loginSuccessHandler)
// 登录成功之后的处理handler
.failureHandler(loginFailureHandler)
// 记住我
.and().rememberMe()
// 配置persistentTokenRepository
.tokenRepository(persistentTokenRepository())
// 配置userDetailsService
.userDetailsService(userDetailsService)
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
创建表
JdbcTokenRepositoryImpl类
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key,token varchar(64) not null, last_used timestamp not null)
设置过期时间
DemoSecurityProperties自定义属性类
@ConfigurationProperties(prefix = "demo.security")
public class DemoSecurityProperties {
/** LoginType登录的方式,默认为JSON(restful风格) **/
private LoginType loginType = LoginType.JSON;
/** Token过期时间为10小时 **/
private int rememberMeSecond = 36000;
public int getRememberMeSecond() {
return rememberMeSecond;
}
public void setRememberMeSecond(int rememberMeSecond) {
this.rememberMeSecond = rememberMeSecond;
}
public LoginType getLoginType() {
return loginType;
}
public void setLoginType(LoginType loginType) {
this.loginType = loginType;
}
}
application.yaml
spring:
# datasource
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springsecurity?useSSL=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: 123456
# jpa
jpa:
show-sql: true
hibernate:
ddl-auto: update
# 自定义配置属性
demo:
security:
# 登录的方式
login-type: json
# Token过期时间
remember-me-second: 3600
SecurityConfig配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 告诉SpringSecurity密码加密方式
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private DataSource dataSource;
@Qualifier("userServiceImpl")
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DemoSecurityProperties demoSecurityProperties;
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
};
@Override
protected void configure(HttpSecurity http) throws Exception {
// 表单登录(身份认证)
http.formLogin()
// 自定义登录页面
.loginPage("/require")
// 如果url为/loginPage,则用SpringSecurity自带的过滤器(UsernamePasswordAuthenticationFilter)来处理该请求
.loginProcessingUrl("/loginPage")
// 登录成功之后的处理handler
.successHandler(loginSuccessHandler)
// 登录成功之后的处理handler
.failureHandler(loginFailureHandler)
// 记住我
.and().rememberMe()
// 配置persistentTokenRepository
.tokenRepository(persistentTokenRepository())
// 设置Token过期秒数
.tokenValiditySeconds(demoSecurityProperties.getRememberMeSecond())
// 配置userDetailsService
.userDetailsService(userDetailsService)
.and()
// 请求授权
.authorizeRequests()
// 访问URL,不需要身份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
// 所有请求
.anyRequest()
// 都需要身份认证
.authenticated()
.and()
// 关闭跨站请求伪造防护
.csrf().disable();
}
}
测试记住我功能
访问/user.html,会跳转到登录界面,进行认证。勾选记住我。
登录成功之后,会发现数据库中的表persistent_logins多了一条数据。
重新启动项目之后。我们再次访问/user.html页面,发现不需要登录了。报404错误,因为没有定义这个页面。
访问/user,也不需要认证,直接访问页面。
这时候所有需要认证的界面都能访问了,不需要认证。记住我功能就实现了。因为使用的是Cookies,只要Token没过期或者数据库中有记录,不管是关闭浏览器还是重启项目,都不需要再次认证。如果用另一个公司的浏览器打开,会需要认证,因为数据库中保存的Token和浏览器中的不一样。