登錄注冊總是密不可分,就放在一起說吧匆绣。數(shù)據(jù)庫中明文密碼自然是不允許的。需要在項目級別攔截請求什黑,來實現(xiàn)登錄注冊操作崎淳。
一、要解決的問題
??本篇要解決的問題
- 項目級別統(tǒng)一攔截請求
- 注冊加密
- 登錄校驗
- 登錄成功/失敗返回自定義信息
- 自定義用戶信息
二兑凿、原理
??Spring Boot項目中引入Spring Security凯力,通過WebSecurityConfigurerAdapter來實現(xiàn)請求的統(tǒng)一攔截,攔截到請求后礼华,通過UserDetailsService來查詢數(shù)據(jù)庫中存儲的用戶信息,比對登錄請求傳輸?shù)男畔⑥置兀瑏泶_定登錄成功與否圣絮。
三、實戰(zhàn)
1.引入Spring Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.0.1.RELEASE</version>
</dependency>
2.自定義WebSecurityConfigurerAdapter統(tǒng)一攔截請求
/**
* @EnableWebSecurity:此注解會啟用Spring Security
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 1)HttpSecurity支持cors雕旨。
* 2)默認(rèn)會啟用CRSF扮匠,此處因為沒有使用thymeleaf模板(會自動注入_csrf參數(shù))捧请,
* 要先禁用csrf,否則登錄時需要_csrf參數(shù)棒搜,而導(dǎo)致登錄失敗疹蛉。
* 3)antMatchers:匹配 "/" 路徑,不需要權(quán)限即可訪問力麸,匹配 "/user" 及其以下所有路徑可款,
* 都需要 "USER" 權(quán)限
* 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查詢數(shù)據(jù)庫中用戶信息
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//從數(shù)據(jù)庫查詢用戶信息
UserInfoBean userInfo = userService.getUser(username);
if (userInfo == null){
throw new UsernameNotFoundException("用戶不存在!");
}
//查詢權(quán)限信息
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);
}
/**
* 權(quán)限字符串轉(zhuǎn)化
*
* 如 "USER,ADMIN" -> SimpleGrantedAuthority("USER") + SimpleGrantedAuthority("ADMIN")
*
* @param roleStr 權(quán)限字符串
*/
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.密碼加密
??密碼加密簡單說明下克蚂。密碼加密分兩個部分闺鲸,注冊時給存儲到數(shù)據(jù)庫的密碼加密和登錄驗證時將拿到的密碼加密與數(shù)據(jù)庫中密碼比對。Spring Security提供了幾種加密方式埃叭,當(dāng)然也可自定義摸恍,此處選用BCryptPasswordEncoder。
??BCryptPasswordEncoder相關(guān)知識:用戶表的密碼通常使用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的子類中指定密碼的加密規(guī)則即可恼琼,Spring Security會自動將密碼加密后與數(shù)據(jù)庫比對。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailService).passwordEncoder(passwordEncoder());
}
/**
* 密碼加密
*/
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
5.自定義用戶登錄流程
??要實現(xiàn)當(dāng)用戶是html結(jié)尾的請求就跳轉(zhuǎn)到默認(rèn)的登錄頁面或者指定的登錄頁屏富,用戶是restFul請求時就返回json數(shù)據(jù)晴竞。備注:此處的校驗邏輯是以html后綴來校驗,如果集成其他模板引擎可根據(jù)需要修改狠半。
1)先定義實現(xiàn)以上需求的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("引發(fā)跳轉(zhuǎn)的請求是:" + 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";//默認(rèn)跳轉(zhuǎn)的接口
// 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)默認(rèn)的登錄頁面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 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)修改認(rèn)證邏輯的代碼見文末
6.自定義登錄成功處理
默認(rèn)情況下Spring Security登錄成功后會跳到之前引發(fā)登錄的請求噩死。修改為登錄成功返回json信息,只需實現(xiàn)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;
/**
* 登錄成功會調(diào)用該方法
* @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;
/**
* 登錄成功會調(diào)用該方法
* @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") //如果需要身份認(rèn)證則跳轉(zhuǎn)到這里
.loginProcessingUrl("/login")
.successHandler(evolutionaryAuthenticationHandler)
.failureHandler(evolutionaryAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/logintype",securityProperties.getBrower().getLoginPage())//不校驗我們配置的登錄頁面
.permitAll()
.anyRequest()
.authenticated()
.and().csrf().disable();
}