spring security默认使用的是用户名密码的登陆,如果想要增加一种登陆方式,
就要仿照源码中用户名密码登陆的方式, 手写一套手机号验证码登陆校验, 话不多说, 开干!
搭建项目基础架构工作, 在(一)中已经完成, 这里直接在其基础上继续开发
这里需要自定义一个token,filter和provider
参考UsernamePasswordAuthenticationToken, 其中principal是用户名, credentials是密码,
由于这里只有手机号, 不需要密码, 所以, 将其中有关credentials的去除, 复制下来即可
自定义SmsCodeAuthenticationToken如下
package com.etouch.security.security.smslogin;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/*
*这一步的作用是为了替换原有系统的 UsernamePasswordAuthenticationToken 用来做验证
*
* 代码都是从UsernamePasswordAuthenticationToken 里粘贴出来的
*
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
/**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
* 在这里就代表登录的手机号码
*/
private final Object principal;
/**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// 剩下的方法不用动就行了 就是从 UsernamePasswordAuthenticationToken 里粘贴出来的
// ========================================================================================================
public Object getCredentials() {
return null;
}
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
同样参照UsernamePasswordAuthenticationFilter,将和password有关的去除, 略微修改以下, 即可完成
自定义的SmsCodeAuthenticationFilter代码如下
package com.etouch.security.security.smslogin;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "phone";
private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
/**
* 是否仅 POST 方式
*/
private boolean postOnly = true;
public SmsCodeAuthenticationFilter() {
// 短信登录的请求 post 方式的 /sms/login
super(new AntPathRequestMatcher("/sys/login/phone", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
// 电话号码
String mobile = obtainUsername(request);
if (StringUtils.isEmpty(mobile)) {
throw new AuthenticationServiceException("电话号码不能为空");
}
return this.getAuthenticationManager().authenticate(new SmsCodeAuthenticationToken(mobile));
}
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(mobileParameter);
}
}
代码如下
package com.etouch.security.security.smslogin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Objects;
/**
* 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
*/
@Configuration
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
String mobile = (String) authenticationToken.getPrincipal();
checkSmsCode(mobile);
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
if (userDetails == null){
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
private void checkSmsCode(String mobile) {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String inputCode = request.getParameter("smsCode");
//这里的验证码我们放session里,这里拿出来跟用户输入的做对比
Map<String, Object> smsLogin = (Map<String, Object>) request.getSession().getAttribute("smsLogin");
if (smsLogin == null) {
throw new BadCredentialsException("未检测到申请验证码");
}
String applyMobile = (String) smsLogin.get("phone");
int code = (int) smsLogin.get("smsCode");
if (!applyMobile.equals(mobile)) {
throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
}
if (code != Integer.parseInt(inputCode)) {
throw new BadCredentialsException("验证码错误");
}
}
@Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
接下来是配置securityConfig
package com.etouch.security.security;
import com.etouch.security.security.handler.*;
import com.etouch.security.pojo.entity.SysPermission;
import com.etouch.security.security.smslogin.SmsCodeAuthenticationFilter;
import com.etouch.security.security.smslogin.SmsCodeAuthenticationProvider;
import com.etouch.security.service.SysPermissionService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import java.util.List;
/**
* UsernamePasswordAuthenticationFilter拦截登录请求
* UsernamePasswordAuthenticationFilter获取到用户名和密码构造一个UsernamePasswordAuthenticationToken传入AuthenticationManager
* AuthenticationManager找到对应的Provider进行具体校验逻辑处理
* 最后登录信息保存进SecurityContext
*
* @author chenyunchang
*/
@Configuration
@EnableWebSecurity
//开启Security注解(使用接口上的注解来控制访问权限)
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
@Slf4j
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//登录成功处理
@Autowired
private SuccessAuthenticationHandler successAuthenticationHandler;
//登录失败处理
@Autowired
private FailureAuthenticationHandler failureAuthenticationHandler;
//未登录处理
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
//没有权限处理
@Autowired
private AuthAccessDeniedHandler authAccessDeniedHandler;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private SysPermissionService sysPermissionService;
@Autowired
private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;
@Autowired
private MyLogoutSuccessHandler myLogoutSuccessHandler;
/**
* 注入身份管理器bean
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码加密器
*/
@Bean
public PasswordEncoder passwordEncoder() {
/**
* BCryptPasswordEncoder:相同的密码明文每次生成的密文都不同,安全性更高
*/
return new BCryptPasswordEncoder();
}
/**
* 表达式 说明
* hasRole([role]) 用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀)
* hasAnyRole([role1,role2]) 用户拥有任意一个制定的角色时返回true
* hasAuthority([authority]) 等同于hasRole,但不会带有ROLE_前缀
* asAnyAuthority([auth1,auth2]) 等同于hasAnyRole
* permitAll 永远返回true
* denyAll 永远返回false
* authentication 当前登录用户的authentication对象
* fullAuthenticated 当前用户既不是anonymous也不是rememberMe用户时返回true
* hasIpAddress('192.168.1.0/24')) 请求发送的IP匹配时返回true
*/
/**
* Http安全配置
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable();
//配置自定义登陆路径
http.formLogin()
//登陆接口
.loginProcessingUrl("/sys/login")
//自定义登陆失败处理
.failureHandler(failureAuthenticationHandler)
//自定义登陆成功处理
.successHandler(successAuthenticationHandler)
//以下是异常处理器
.and().exceptionHandling()
//未登录自定义返回
.authenticationEntryPoint(customAuthenticationEntryPoint)
//没有权限访问处理
.accessDeniedHandler(authAccessDeniedHandler)
.and().logout().logoutUrl("/sys/logout")
;
//短信验证码登陆
// http.apply(smsCodeAuthenticationSecurityConfig)
// .and().formLogin().loginProcessingUrl("/sys/login/phone")
// ;
//短信验证码登陆验证
//添加手机号登陆过滤器
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
//查询所有权限,动态权限认证
List<SysPermission> permissions = sysPermissionService.list();
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
.authorizeRequests();
permissions.forEach(permission ->
{
log.info("获取权限为" + permission.getPermCode());
//将连接地址对应的权限存入
authorizeRequests.antMatchers(permission.getUrl()).hasAnyAuthority(permission.getPermCode());
});
//配置无需认证的访问路径
http.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// 登录URL
.antMatchers("/login/**").permitAll()
.antMatchers("/sms/**").permitAll()
// swagger
.antMatchers("/swagger**/**").permitAll()
.antMatchers("/webjars/**").permitAll()
.antMatchers("/v2/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(myLogoutSuccessHandler);
}
/**
* 配置无需登陆就可以访问的路径
*
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
//allow Swagger URL to be accessed without authentication
web.ignoring().antMatchers(
//swagger api json
"/v2/api-docs",
//用来获取支持的动作
"/swagger-resources/configuration/ui",
//用来获取api-docs的URI
"/swagger-resources",
//安全选项
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/doc.html",
"/css/**", "/js/**"
);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 这里要设置自定义认证
//手机号验证
auth.authenticationProvider(smsCodeAuthenticationProvider);
}
}
package com.etouch.security.security;
import com.etouch.security.pojo.dto.SysRoleDTO;
import com.etouch.security.pojo.dto.SysUserDTO;
import com.etouch.security.service.SysUserService;
import com.etouch.security.util.MobileUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
/**
* 自定义的认证用户获取服务类
*/
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
/**
* 根据用户名获取认证用户信息
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (StringUtils.isEmpty(username)) {
log.info("UserDetailsService没有接收到用户账号");
throw new UsernameNotFoundException("UserDetailsService没有接收到用户账号");
} else {
//根据用户名查找用户信息
SysUserDTO sysUserDTO = null;
if (MobileUtil.isMobileNO(username)) {
//手机号验证码登陆
sysUserDTO = sysUserService.getUserByPhone(username);
} else {
//用户名, 密码登陆
sysUserDTO = sysUserService.getUserByUserName(username);
}
if (sysUserDTO == null) {
throw new UsernameNotFoundException(String.format("用户不存在", username));
}
//新建权限集合
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
//模拟从数据库获取角色权限
List<SysRoleDTO> sysRoleDTOList = sysUserDTO.getSysRoleDTOList();
for (SysRoleDTO sysRoleDTO : sysRoleDTOList) {
//封装用户信息和角色信息到SecurityContextHolder全局缓存中
grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleDTO.getRoleName()));
}
//创建一个用于认证的用户对象并返回,包括:用户名,密码,角色
return new User(sysUserDTO.getUsername(), sysUserDTO.getPassword(), grantedAuthorities);
}
}
}
package com.etouch.security.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/sms")
@Slf4j
@Api(tags = "短信接口")
public class SmsController {
@ApiOperation("发送短信验证码")
@RequestMapping(value = "/code",method = RequestMethod.POST)
public String sms(String phone, HttpServletRequest request) {
HttpSession session = request.getSession();
int smsCode = (int) Math.ceil(Math.random() * 9000 + 1000);
Map<String, Object> map = new HashMap<>(16);
map.put("phone", phone);
map.put("smsCode", smsCode);
session.setAttribute("smsLogin", map);
log.info("{}:为 {} 设置短信验证码:{}", session.getId(), phone, smsCode);
return "你的手机号"+phone+"验证码是"+smsCode;
}
}
package com.etouch.security.controller;
import com.etouch.security.exception.ExceptionEnum;
import com.etouch.security.exception.ProjectException;
import com.etouch.security.pojo.dto.SysUserDTO;
import com.etouch.security.security.smslogin.SmsCodeAuthenticationToken;
import com.etouch.security.service.SysUserService;
import com.etouch.security.util.ResultUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.catalina.security.SecurityUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.security.Security;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
/**
* @author chenyunchang
* @title
* @date 2020/10/28 11:10
* @Description:
*/
@RestController
@RequestMapping
@Api(tags = "登陆相关接口")
public class LoginController {
@Autowired
private SysUserService sysUserService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@ApiOperation("用户名,密码登陆")
@PostMapping("/login")
public ResultUtils<SysUserDTO> login(@RequestParam String username, @RequestParam String password, HttpServletRequest request) {
SysUserDTO sysUserDTO = sysUserService.getUserByUserName(username);
if (sysUserDTO == null) {
throw new ProjectException(ExceptionEnum.USER_NOT_FOUND);
}
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
boolean matches = bCryptPasswordEncoder.matches(password, sysUserDTO.getPassword());
if (!matches) {
throw new ProjectException(ExceptionEnum.USERNAME_OR_PASSWORD_ERRO);
}
sysUserDTO.setPassword(null);
// 系统登录认证
return ResultUtils.success("登陆成功", sysUserDTO);
}
@ApiOperation("手机号, 验证码登陆")
@PostMapping("/login/phone")
public ResultUtils<SysUserDTO> loginByPhoneAndCode(String phone, String smsCode, HttpServletRequest request) {
//security已经错过校验, 这里不再校验验证码
SysUserDTO sysUserDTO = sysUserService.getUserByPhone(phone);
if (sysUserDTO == null) {
throw new ProjectException(ExceptionEnum.USER_NOT_FOUND);
}
sysUserDTO.setPassword(null);
//进行手动security登陆
UserDetails userDetails = userDetailsService.loadUserByUsername(sysUserDTO.getUsername());
SmsCodeAuthenticationToken smsCodeAuthenticationToken = new SmsCodeAuthenticationToken(phone, userDetails.getAuthorities());
Authentication authenticate = authenticationManager.authenticate(smsCodeAuthenticationToken);
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(authenticate);
HttpSession session = request.getSession(true);
//在session中存放security context,方便同一个session中控制用户的其他操作
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
return ResultUtils.success("登陆成功", sysUserDTO);
}
}