自定义认证逻辑
1.认证逻辑接口
spring-security用户登录逻辑验证接口org.springframework.security.core.userdetails.UserDetailsService只有一个方法
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
UserDetail信息如下:我们自定义的用户信息要实现这个接口,
public interface UserDetails extends Serializable {
//权限相关
Collection<? extends GrantedAuthority> getAuthorities();
//获取密码
String getPassword();
//获取用户名
String getUsername();
//账户是否验证过期
boolean isAccountNonExpired();
//账户是否锁定
boolean isAccountNonLocked();
//账户验证是否过期
boolean isCredentialsNonExpired();
//账户是否有效
boolean isEnabled();
}
org.springframework.security.core.userdetails.User这个是它的一个实现
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));//线程安全的权限添加 同时有内部类自定义排序
2.处理密码加密解密
配置了这个Bean
以后,从前端传递过来的密码就会被加密,所以从数据库查询到的密码必须是经过加密的,而这个过程都是在用户注册的时候进行加密的。这就合理解释了为什么对上面的代码进行加密了。
org.springframework.security.crypto.password.PasswordEncoder
public interface PasswordEncoder { //加密 String encode(CharSequence var1); //验证是否匹配 boolean matches(CharSequence var1, String var2); }
在浏览器权限配置类BrowserSecurityConfig中注入这个bean
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密解密
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
//spring5后默认就是表单登录方式
// httpBasic().
formLogin().
and().
authorizeRequests().
anyRequest().
authenticated();
}
}
3.自定义接口实现
package com.rui.tiger.auth.browser.user;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.Component;
/**
* 自定义用户登录实现
*
* @author CaiRui
* @date 2018-12-5 8:19
*/
@Component
@Slf4j
public class MyUserDetailServiceImpl implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//TODO 后续做成数据库实现(MyBaites-plus实现)先实现流程
//1.根据用户名去数据库去查询用户信息获取加密后的密码 这里模拟一个加密的数据库密码
String encryptedPassWord = passwordEncoder.encode("123456");
log.info("模拟加密后的数据库密码:{}",encryptedPassWord);
//2.这里可以去验证账户的其它相关信息 默认都通过
//3.返回认证过的用户信息 授予一个admin的权限
return new User(username,
encryptedPassWord,
true,
true,
true,
true,
AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
实现完了我们启动项目来验证下配置的MyUserDetailServiceImpl是否成功了,可以看到默认的随机密码在控制台已经没有了。浏览器随便访问一个地址,会调到默认的登录表单界面
密码我们先随便输入一个 比如66666
可以看到登录失败,我们再输入我们固定的密码123456
可以看到我们登录成功,所以出现这个界面是因为http://localhost:8070/user这个我没有实现,验证成功后重定向到之前的地址 同时我们可以看到控制台也会打印如下信息 证明我们的自定义认证成功。ok下面我们开始实现自己的个性化登录需求开发
4.个性化登录实现
在实际开发中通常我们都不会使用spring-security默认的登录界面,我们可以通过配置实现自己的个性化登录,下面是具体实现。
1)自定义登录页面
首先修改我们的浏览器配置类BrowserSecurityConfig,同时要在资源文件下添加我们的自定义登录界面/tiger-login.html
package com.rui.tiger.auth.browser.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;
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/tiger-login.html")//自定义标准登录界面
.and()
.authorizeRequests()
.antMatchers("/tiger-login.html")//此路径放行 否则会陷入死循环
.permitAll()
.anyRequest()
.authenticated();
}
}
tiger-login.html文件如下,注意放置的路径
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标准登录页面</title>
</head>
<body>
<h2>标准登录页面</h2>
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
ok 我们来启动项目输入http://localhost:8070/user 看看效果,可以看见已经成功跳到我们的自定义界面了
我们再次输入用户名user和密码123456试试看
可以看见又重定向到我们的tiger-login.html,这是怎么回事呢?
原来是是我们的 tiger-login.html定义的表单请求<form action="/authentication/form" method="post">和spring-security默认的表单登录请求不一致,参见UsernamePasswordAuthenticationFilter源码如下:
public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); }
我们只要BrowserSecurityConfig添加自定义表单的请求路径就可以loginProcessingUrl("/authentication/form"),同时进行权限放行,并关闭跨域访问,相关配置如下
package com.rui.tiger.auth.browser.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;
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/tiger-login.html")//自定义标准登录界面
.loginProcessingUrl("/authentication/form")//自定义表单请求路径
.and()
.authorizeRequests()
.antMatchers("/tiger-login.html","/authentication/form")//此路径放行 否则会陷入死循环
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域关闭
;
}
}
再次访问 localhost:8070/user/hello 可以看到api可以成功访问了
这里虽然配置了自定义的路径,但都是统一跳转到了静态界面,在现在流行的前后台分离的项目中,返回给前台的通常都是一个json串,那么要怎么实现 根据请求来分发是返回html内容?还是返回json内容呢?
处理不同类型的请求
由于我们程序中有很多信息来自配置文件,下面我们用类来统一管理请看下面实现,先看下他们的关系
SecurityPropertie 权限配置父类
BrowserProperties 浏览器相关配置
AppProperties 移动端相关配置
SocialProperties 社交相关配置
CaptchaProperties 验证码相关配置
。。。。。。。。。。
由于这些配置类是browser和app项目公用的,所以写在核心模块core里
package com.rui.tiger.auth.core.properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 权限配置文件父类(注意这里不用lombok 会读取不到)
* 这里会有很多权限配置子模块
* @author CaiRui
* @date 2018-12-6 8:41
*/
@ConfigurationProperties(value = "tiger.auth",ignoreInvalidFields = true)
public class SecurityProperties {
/**
* 浏览器配置类
*/
private BrowserProperties browser = new BrowserProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
}
BrowserProperties 浏览器配置如下:
package com.rui.tiger.auth.core.properties;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
/**
* 浏览器配置
*
* @author CaiRui
* @date 2018-12-6 8:42
*/
public class BrowserProperties {
/**
* 登录页面 不配置默认标准登录界面
*/
private String loginPage = "/tiger-login.html";
/**
* 跳转类型 默认返回json数据
*/
private LoginTypeEnum loginType = LoginTypeEnum.JSON;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
public LoginTypeEnum getLoginType() {
return loginType;
}
public void setLoginType(LoginTypeEnum loginType) {
this.loginType = loginType;
}
}
还要一个配置类SecurityPropertiesCoreConfig来使上面的配置生效
package com.rui.tiger.auth.core.config;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* SecurityProperties 配置类注入生效
*
* @author CaiRui
* @date 2018-12-6 8:57
*/
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityPropertiesCoreConfig {
}
项目application.yml配置文件如下配置
spring:
datasource:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://my.yunout.com:3306/tiger_study?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: root
# 配置Druid连接池
type: com.alibaba.druid.pool.DruidDataSource
session:
store-type: none
# Tomcat
server:
port: 8070
connection-timeout: 5000ms
#自定义权限配置
tiger:
auth:
browser:
#loginPage: /demo-login.html # 这里可以配置成自己的非标准登录界面
loginType: JSON
LoginTypeEnum是BrowserProperties中控制跳转行为的枚举类
package com.rui.tiger.auth.core.model.enums;
import lombok.Getter;
/**
* 登录类型枚举类
* @author CaiRui
* @date 2018-12-6 12:45
*/
@Getter
public enum LoginTypeEnum {
/**
* json数据返回
*/
JSON,
/**
* 重定向
*/
REDIRECT;
}
ok 上面权限配置类都准备完成了,修改浏览器配置类,使其登录路径是我们自定义的控制器路径,里面控制是返回josn 还是html界面,
同时里面还要我们自定义的登录成功和失败处理器,这个我们稍后来说。
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.properties.SecurityProperties;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 浏览器security配置类
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
/**
* 密码加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage( "/authentication/require")//自定义登录请求
.loginProcessingUrl("/authentication/form")//自定义表单登录地址
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/authentication/require")//此路径放行 否则会陷入死循环
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域关闭
;
}
}
编写处理请求的处理器BrowserRequireController
package com.rui.tiger.auth.browser.controller;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 用户登录认证控制器
*
* @author CaiRui
* @date 2018-12-5 12:44
*/
@RestController
@Slf4j
public class BrowserRequireController {
//封装了引发跳转请求的工具类 https://blog.csdn.net/honghailiang888/article/details/53671108
private RequestCache requestCache = new HttpSessionRequestCache();
// spring的工具类:封装了所有跳转行为策略类
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
private static final String HTML_SUFFIX = ".html";
/**
* 当需要进行身份认证的时候跳转到此方法
*
* @param request 请求
* @param response 响应
* @return 将信息以JSON形式返回给前端
*/
@RequestMapping("/authentication/require")
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
log.info("BrowserRequireController进来了 啦啦啦");
// 从session缓存中获取引发跳转的请求
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (null != savedRequest) {
String redirectUrl = savedRequest.getRedirectUrl();
log.info("引发跳转的请求是:{}", redirectUrl);
if (StringUtils.endsWithIgnoreCase(redirectUrl, HTML_SUFFIX)) {
// 如果是HTML请求,那么就直接跳转到HTML,不再执行后面的代码
redirectStrategy.sendRedirect(request, response, securityProperties.getBrowser().getLoginPage());
}
}
return new SimpleResponse("访问的服务需要身份认证,请引导用户到登录页面");
}
}
同时编写我们的登录成功TigerAuthenticationSuccessHandler和失败处理器TigerAuthenticationFailureHandler,这里可以加入我们的一些逻辑 比如登录成功记录日志,这里只是返回json还是重定向处理,通过配置 BrowserProperties中的loginType就可以实现,参看上面。
TigerAuthenticationSuccessHandler
package com.rui.tiger.auth.core.authentication;
import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证成功处理器
* {@link SavedRequestAwareAuthenticationSuccessHandler}是Spring Security默认的成功处理器
* @author CaiRui
* @date 2018-12-6 12:39
*/
@Component("tigerAuthenticationSuccessHandler")
@Slf4j
public class TigerAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
log.info("登录成功");
if(LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())){
//返回json处理 默认也是json处理
response.setContentType("application/json;charset=UTF-8");
log.info("认证信息:"+JSON.toJSONString(authentication));
response.getWriter().write(JSON.toJSONString(authentication));
} else {
// 如果用户定义的是跳转,那么就使用父类方法进行跳转
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
TigerAuthenticationFailureHandler
package com.rui.tiger.auth.core.authentication;
import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 认证失败处理器
* @author CaiRui
* @date 2018-12-6 12:40
*/
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登录失败");
if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(exception));
} else {
// 如果用户配置为跳转,则跳到Spring Boot默认的错误页面
super.onAuthenticationFailure(request, response, exception);
}
}
}
ok 下面我们来测试下看我们的流程是否可以?
如果我们直接访问 localhost:8070/user/hello
这是因为我们默认配置了json,输入我们的的登录表单地址localhost:8070/tiger-login.html,并输入正确的账户密码登录
可以看到已经返回认证成功的json字符串,失败处理器也会返回失败的信息这里就不测试了。
到现在整个登录基本流程算是跑通了,下一章我们来简单分析下spring-security的认证源码。
‘’
TigerAuthenticationFailureHandlerTigerAuthenticationFailureHandler