登录注册总是密不可分,就放在一起说吧。数据库中明文密码自然是不允许的。需要在项目级别拦截请求,来实现登录注册操作。
一、要解决的问题
本篇要解决的问题
- 项目级别统一拦截请求
- 注册加密
- 登录校验
- 登录成功/失败返回自定义信息
- 自定义用户信息
二、原理
Spring Boot项目中引入Spring Security,通过WebSecurityConfigurerAdapter来实现请求的统一拦截,拦截到请求后,通过UserDetailsService来查询数据库中存储的用户信息,比对登录请求传输的信息,来确定登录成功与否。
三、实战
1.引入Spring Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
2.自定义WebSecurityConfigurerAdapter统一拦截请求
/**
* @EnableWebSecurity:此注解会启用Spring Security
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 1)HttpSecurity支持cors。
* 2)默认会启用CRSF,此处因为没有使用thymeleaf模板(会自动注入_csrf参数),
* 要先禁用csrf,否则登录时需要_csrf参数,而导致登录失败。
* 3)antMatchers:匹配 "/" 路径,不需要权限即可访问,匹配 "/user" 及其以下所有路径,
* 都需要 "USER" 权限
* 4)配置登录地址和退出地址
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/user/**").hasRole("USER")
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/hello")
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
}
3.自定义UserDetailsService查询数据库中用户信息
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//从数据库查询用户信息
UserInfoBean userInfo = userService.getUser(username);
if (userInfo == null){
throw new UsernameNotFoundException("用户不存在!");
}
//查询权限信息
List<SimpleGrantedAuthority> simpleGrantedAuthorities = createAuthorities(userInfo.getRoles());
//返回Spring Security框架提供的User或者自定义的MyUser(implements UserDetails)
//// return new MyUser(username, userInfo.getPassword(), simpleGrantedAuthorities);
return new User(username, userInfo.getPassword(), simpleGrantedAuthorities);
}
/**
* 权限字符串转化
*
* 如 "USER,ADMIN" -> SimpleGrantedAuthority("USER") + SimpleGrantedAuthority("ADMIN")
*
* @param roleStr 权限字符串
*/
private List<SimpleGrantedAuthority> createAuthorities(String roleStr){
String[] roles = roleStr.split(",");
List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
for (String role : roles) {
simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role));
}
return simpleGrantedAuthorities;
}
}
4.密码加密
密码加密简单说明下。密码加密分两个部分,注册时给存储到数据库的密码加密和登录验证时将拿到的密码加密与数据库中密码比对。Spring Security提供了几种加密方式,当然也可自定义,此处选用BCryptPasswordEncoder。
BCryptPasswordEncoder相关知识:用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。BCrypt算法将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题。
1)注册时给密码加密
注册接口中加密密码:
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public boolean insert(UserInfoBean userInfo){
encryptPassword(userInfo);
if(userInfoMapper.insert(userInfo)==1)
return true;
else
return false;
};
private void encryptPassword(UserInfoBean userInfo){
String password = userInfo.getPassword();
password = new BCryptPasswordEncoder().encode(password);
userInfo.setPassword(password);
}
}
2)登录时密码加密校验
只需在WebSecurityConfigurerAdapter的子类中指定密码的加密规则即可,Spring Security会自动将密码加密后与数据库比对。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
/**
* 密码加密
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
5.自定义用户登录流程
要实现当用户是html结尾的请求就跳转到默认的登录页面或者指定的登录页,用户是restFul请求时就返回json数据。备注:此处的校验逻辑是以html后缀来校验,如果集成其他模板引擎可根据需要修改。
1)先定义实现以上需求的controller逻辑
private RequestCache requestCache = new HttpSessionRequestCache();
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping("/logintype")
@ResponseBody
@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest != null) {
String targetUrl = savedRequest.getRedirectUrl();
logger.info("引发跳转的请求是:" + targetUrl);
if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
redirectStrategy.sendRedirect(request, response, securityProperties.getBrower().getLoginPage());
}
}
return "请登录!";
}
@GetMapping("/login_html")
public String loginHtml(){
return "login";
}
@PostMapping("/login")
public void login(){
}
2)定义不同登录页面的配置类
@Configuration
@ConfigurationProperties(prefix = "evolutionary.security")
public class SecurityProperties {
private BrowerProperties brower = new BrowerProperties();
public BrowerProperties getBrower() {
return brower;
}
public void setBrower(BrowerProperties brower) {
this.brower = brower;
}
}
public class BrowerProperties {
private String loginPage = "/login_html";//默认跳转的接口
// private LoginInType loginInType = LoginInType.JSON;
public String getLoginPage() {
return loginPage;
}
public void setLoginPage(String loginPage) {
this.loginPage = loginPage;
}
// public LoginInType getLoginInType() {
// return loginInType;
// }
//
// public void setLoginInType(LoginInType loginInType) {
// this.loginInType = loginInType;
// }
}
可配置的登录页面
#配置登录页面接口
#evolutionary.security.brower.loginPage = /login_html
#evolutionary.security.brower.loginInType=REDIRECT
3)默认的登录页面login.html
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 | SpringForAll - Spring Security</title>
<link href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.css" rel="stylesheet">
</head>
<body style="background-color: #f1f1f1; padding-bottom: 0">
<div class="container" style="margin-top: 60px">
<div class="row" style="margin-top: 100px">
<div class="col-md-6 col-md-offset-3">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title"><span class="glyphicon glyphicon-console"></span> Login</h3>
</div>
<div class="panel-body">
<form th:action="@{/login}" method="post">
<div class="form-group" style="margin-top: 30px">
<div class="input-group col-md-6 col-md-offset-3">
<div class="input-group-addon"><span class="glyphicon glyphicon-user"></span></div>
<input type="text" class="form-control" name="username" id="username" placeholder="账号">
</div>
</div>
<div class="form-group ">
<div class="input-group col-md-6 col-md-offset-3">
<div class="input-group-addon"><span class="glyphicon glyphicon-lock"></span></div>
<input type="password" class="form-control" name="password" id="password"
placeholder="密码">
</div>
</div>
<br>
<div class="form-group">
<div class="input-group col-md-6 col-md-offset-3 col-xs-12 ">
<button type="submit" class="btn btn-primary btn-block">登录</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
4)修改认证逻辑的代码见文末
6.自定义登录成功处理
默认情况下Spring Security登录成功后会跳到之前引发登录的请求。修改为登录成功返回json信息,只需实现AuthenticationSuccessHandler接口的onAuthenticationSuccess方法:
@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* spring MVC 启动的时候会为我们注册一个objectMapper
*/
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
/**
* 登录成功会调用该方法
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功!");
if (LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else{
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
@Component("evolutionaryAuthenticationHandler")
public class EvolutionaryAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* spring MVC 启动的时候会为我们注册一个objectMapper
*/
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
/**
* 登录成功会调用该方法
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功!");
if (/*LoginInType.JSON.equals(securityProperties.getBrower().getLoginInType())*/1==1) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}else{
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
再配置登录成功的处理方式:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() //表单登录
//.loginPage("/evolutionary-loginIn.html")
.loginPage("/logintype") //如果需要身份认证则跳转到这里
.loginProcessingUrl("/login")
.successHandler(evolutionaryAuthenticationHandler)
.failureHandler(evolutionaryAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校验我们配置的登录页面
.permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable();
}