Spring Security 實戰筆記

久聞Spring Security 很強大,一直沒有機會再實際項目中用到。這次有幸肚子擔當一個項目的登錄,權限,根據權限顯示頁面目錄等功能,再此將開發的核心代碼記錄一下,方便以後參考。
首先說明項目是spring boot 所以講maven依賴jar包引入。

<dependency>                                             
    <groupId>org.springframework.boot</groupId>          
    <artifactId>spring-boot-starter-web</artifactId>     
</dependency>                                            
<dependency>                                             
    <groupId>org.springframework.boot</groupId>          
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>                                            
<dependency>                                           
    <groupId>org.springframework.boot</groupId>          
    <artifactId>spring-boot-starter-cache</artifactId>   
</dependency>                                            
<dependency>                                             
    <groupId>org.springframework.security</groupId>      
    <artifactId>spring-security-taglibs</artifactId>     
</dependency>                                            

導入maven後可以用spring security默認提供的登錄頁面和臨時的密碼可以試玩一次。(感興趣的話可以自行BaiDu 資料很多這裏就不細說了)
SecurityConfig 核心配置類(配置登錄,註冊自定義驗證,成功失敗等配置類)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.configuration.WebSecurityConfigurerAdapter;

import com.kgsettle.security.service.SecurityAuthenticationFailHandler;
import com.kgsettle.security.service.SecurityAuthenticationProvider;
import com.kgsettle.security.service.SecurityAuthenticationSuccessHandler;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true)
public class SecurityWebConfig extends WebSecurityConfigurerAdapter {

	@Autowired
    private SecurityAuthenticationProvider kgSettleAuthenticationProvider;//自定義驗證類
	@Autowired
	private SecurityAuthenticationSuccessHandler securityAuthenticationSuccessHandler;//自定義登錄成功類
	@Autowired
	private SecurityAuthenticationFailHandler securityAuthenticationFailHandler;//自定義登錄失敗類

	@Override
    protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable()
			.formLogin().loginPage("/login").loginProcessingUrl("/login-process")
			.successHandler(securityAuthenticationSuccessHandler)//註冊登錄成功Handler
			.failureHandler(securityAuthenticationFailHandler)//註冊登錄失敗Handler
			.permitAll()
			.and()
			.logout().logoutUrl("/logout")//定義註銷url
			.invalidateHttpSession(true)
			.clearAuthentication(true)
			.and()
			.authorizeRequests()
			.antMatchers("/", "/error", "/error/*", "/**/*.*")//不需要攔截的url
			.permitAll()
			.anyRequest().authenticated();
	}

	@Autowired
	public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.authenticationProvider(kgSettleAuthenticationProvider);//註冊自定義登錄驗證
	}
}

自定義Security用戶類實現 UserDetails

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.kgsettle.common.enumeration.StatusType;
import com.kgsettle.common.enumeration.UserType;

import lombok.Data;

/**
 * Security User Model
 */
@Data
public class SecurityUser implements UserDetails {
	private static final long serialVersionUID = -7198432015491655313L;
	/** 主鍵 */
    private Long userNo;
    /** 賬號 */
    private String userId;
    /** 狀態 */
    private StatusType status;
    /** 用戶級別 */
    private UserType userType;
    /** 最近登錄 IP */
    private String lastIp;
    /** 最近登錄時間 */
    private Date lastDate;
// 用戶類型-權限(spring security權限默認會以ROLE_開頭,所以註冊時候得加上ROLE_)
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		List<SimpleGrantedAuthority> simpleAuthorities = new ArrayList<SimpleGrantedAuthority>();
		simpleAuthorities.add(new SimpleGrantedAuthority("ROLE_" + this.userType.toString()));
		return simpleAuthorities;
	}

	@Override
	public String getPassword() {
		return null;
	}
	@Override
	public String getUsername() {
		return this.userId;
	}
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}
	@Override
	public boolean isAccountNonLocked() {
		return true;
	}
	@Override
	public boolean isCredentialsNonExpired() {
		return true;
	}
	@Override
	public boolean isEnabled() {
		return true;
	}

}
----------------------用戶類型(管理員,一般用戶,合作方)-------------------------
public enum UserType implements KeyEnum, TextEnum {

	ADMIN(0, "管理員"),
	USER(1, "一般用戶"),
	PARTNER(2, "合作方");

    private int key;
    private String text;


    private UserType(int key, String text) {
        this.key = key;
        this.text = text;
    }

    public int getKey() {
        return key;
    }

    public String getText() {
        return text;
    }

    public static Map<Integer, UserType> getUserType() {
        Map<Integer, UserType> userTypeMap = new HashMap<Integer, UserType>();
        for (UserType userType : values()) {
        	userTypeMap.put(userType.key, userType);
        }
        return userTypeMap;
    }
}

自定義登錄驗證類(可根據自己需求驗證用戶名和密碼,還有用戶的狀態等)

import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import com.kgsettle.common.enumeration.StatusType;
import com.kgsettle.common.exception.ErrorType;
import com.kgsettle.security.model.RestResponseModel;
import com.kgsettle.security.model.SecurityUser;

@Component
public class SecurityAuthenticationProvider implements AuthenticationProvider {

	@Value("https://ldapgw.kakaogames.io/v1/authorize")
	String apiUrl;
	@Autowired
	RestTemplate restTemplate;
	@Autowired
	SecurityUserDetailService kgSettleUserDetailService;

	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		String username = authentication.getName().trim();
        String password = authentication.getCredentials().toString();
        HashMap<String, String> params = new HashMap<String, String>();
        params.put("id", username);
        params.put("pw", password);
        try {
        	RestResponseModel restResponse = restTemplate.postForObject(apiUrl, params, RestResponseModel.class);
        	if (restResponse.getStatus() != 200) { // 通過rest api 從第三方平臺驗證賬號密碼
        		throw new BadCredentialsException(ErrorType.USER_LOGIN_LDAP_ERR.getCode());
        	}
		} catch (Exception e) {
			throw new BadCredentialsException(ErrorType.USER_LOGIN_LDAP_ERR.getCode());
		}
        SecurityUser securityUser = (SecurityUser) kgSettleUserDetailService.loadUserByUsername(username);
        if (securityUser == null) { // 判斷當前項目中是否登錄用戶信息
        	throw new DisabledException(ErrorType.USER_LOGIN_REGISTER_ERR.getCode());
        }
        if (StatusType.INACTIVE.equals(securityUser.getStatus())) { // 判斷賬號狀態是否被鎖定
        	throw new LockedException(ErrorType.USER_LOGIN_STATUS_ERR.getCode());
        }
		return new UsernamePasswordAuthenticationToken(username, password, securityUser.getAuthorities());
	}

	@Override
	public boolean supports(Class<?> authentication) {
		return true;
	}

}

自定義登錄成功Handler(登錄成功後可以按自己的需求做一些事情比如保存用戶登錄時間和登錄ip等記錄)
 

import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import com.kgsettle.security.model.SecurityUser;
import com.kgsettle.user.dao.UserDao;

@Component
public class SecurityAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

	@Autowired
	UserDao userDao;

	@Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
		try {
			SecurityUser securityUser = userDao.selectSecurityUserById(authentication.getName());
			securityUser.setLastIp(getRealIp(request));
			userDao.updateSecurityUser(securityUser);
			Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
			if (roles.contains("ROLE_ADMIN")) {
				response.sendRedirect("user/list");
			} else if (roles.contains("ROLE_USER")) {
				response.sendRedirect("ingame/ingameList");
			} else if(roles.contains("ROLE_PARTNER")) {
				response.sendRedirect("provisionalSettle/provisionalSettleList");
			} else {
				response.sendRedirect("error");
			}
		} catch (Exception e) {
			response.sendRedirect("error");
		}
	}

	private String getRealIp(HttpServletRequest request) {
		Map<String, String> result = new HashMap<>();
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String key = headerNames.nextElement();
            String value = request.getHeader(key);
            result.put(key, value);
        }
		if (StringUtils.isNotBlank(request.getHeader("x-forwarded-for"))) {
			return request.getHeader("x-forwarded-for").split(",")[0];
		}
		if (StringUtils.isNotBlank(request.getHeader("X-Real-IP"))) {
			return request.getHeader("X-Real-IP");
		}
		return request.getRemoteAddr();
	}
}

自定義登錄失敗Handler(登錄失敗後將錯誤信息暫時保存到session中跳轉到登錄頁面時顯示具體錯誤信息)

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Component
public class SecurityAuthenticationFailHandler extends SimpleUrlAuthenticationFailureHandler {

	@Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
    		AuthenticationException exception) throws IOException, ServletException {
		request.getSession().setAttribute("errCode", exception.getMessage());
		response.sendRedirect("/login");
	}

}

自定義UserDetailsService(重寫loadUserByUsername方法查詢數據庫返回自定義的UserDetails對象,可以對密碼加密等我這裏由於系統是公司內部使用安全級別不需要那麼高所以沒有加密)

import org.springframework.beans.factory.annotation.Autowired;
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 com.kgsettle.user.dao.UserDao;

@Service
public class SecurityUserDetailService implements UserDetailsService {

	@Autowired
    UserDao userDao;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		return userDao.selectSecurityUserById(username);
	}

}

自定義用戶登錄頁面Controller

import java.util.Set;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

/**
 * 用戶登錄 Controller
 */
@Controller
public class SecurityWebController {

	private static final Pattern MOBILE_TABLET_B = Pattern.compile("android|avantgo|bada\\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(ad|hone|od)|iris|kindle|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\\/|plucker|pocket|psp|symbian|treo|up\\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino|playbook|silk", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
	private static final Pattern MOBILE_TABLET_V = Pattern.compile("1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\\-(n|u)|c55\\/|capi|ccwa|cdm\\-|cell|chtm|cldc|cmd\\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\\-s|devi|dica|dmob|do(c|p)o|ds(12|\\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\\-|_)|g1 u|g560|gene|gf\\-5|g\\-mo|go(\\.w|od)|gr(ad|un)|haie|hcit|hd\\-(m|p|t)|hei\\-|hi(pt|ta)|hp( i|ip)|hs\\-c|ht(c(\\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\\-(20|go|ma)|i230|iac( |\\-|\\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\\/)|klon|kpt |kwc\\-|kyo(c|k)|le(no|xi)|lg( g|\\/(k|l|u)|50|54|e\\-|e\\/|\\-[a-w])|libw|lynx|m1\\-w|m3ga|m50\\/|ma(te|ui|xo)|mc(01|21|ca)|m\\-cr|me(di|rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\\-2|po(ck|rt|se)|prox|psio|pt\\-g|qa\\-a|qc(07|12|21|32|60|\\-[2-7]|i\\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\\-|oo|p\\-)|sdk\\/|se(c(\\-|0|1)|47|mc|nd|ri)|sgh\\-|shar|sie(\\-|m)|sk\\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\\-|v\\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\\-|tdg\\-|tel(i|m)|tim\\-|t\\-mo|to(pl|sh)|ts(70|m\\-|m3|m5)|tx\\-9|up(\\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|xda(\\-|2|g)|yas\\-|your|zeto|zte\\-", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE);
	
    /**
     * 用戶登錄頁面
     *
     * @param model
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/login")
    public ModelAndView login(Model model, HttpServletRequest request) {
    	if (isPC(request.getHeader("user-agent"))) {//只允許PC端訪問系統
    		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    		if (authentication != null && !StringUtils.equals(authentication.getName(), "anonymousUser")) {
    			Set<String> roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities());
    			if (roles.contains("ROLE_ADMIN")) {//根據登錄用戶權限跳轉默認頁面
    				return new ModelAndView("redirect:user/list");
    			} else if (roles.contains("ROLE_USER")) {
    				return new ModelAndView("redirect:ingame/ingameList");
    			} else if (roles.contains("ROLE_PARTNER")) {
    				return new ModelAndView("redirect:provisionalSettle/provisionalSettleList");
    			}
    		}
    		HttpSession session = request.getSession(false);
    		if (session != null) {
				Object errCode = session.getAttribute("errCode");
				if (errCode != null) {
					model.addAttribute("errCode", errCode);
					session.removeAttribute("errCode");
				}
			}
    		return new ModelAndView("etc/member/login");
    	}
    	return new ModelAndView("redirect:/error");
    }

    private boolean isPC(String userAgent) {
    	if (userAgent != null && (MOBILE_TABLET_B.matcher(userAgent).find() ||
                userAgent.length() >= 4 && MOBILE_TABLET_V.matcher(userAgent.substring(0, 4)).find())) {
    		return false;
    	}
    	return true;
    }
}

Spring Boot domain訪問首頁設置成登陸頁面

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class DefaultLoginConfig implements WebMvcConfigurer {

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
        registry.addRedirectViewController("/", "/login");
	}
}

系統layout使用tiles框架來佈局頁面從而將菜單欄的jsp根據用戶類型分成3個jsp來通過Spring Security標籤來根據權限生成頁面(jsp中權限可以省略“ROLE_”但是後臺必須要加上“ROLE_”)

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<!DOCTYPE html>
<html lang="ko">

<head>
    <tiles:insertAttribute name="header" />
</head>

<body class="layout-fixed">
    <div class="wrapper">
        <tiles:insertAttribute name="top" />
        <sec:authorize access="hasRole('ADMIN')">
        <tiles:insertAttribute name="gnb_admin" />
        </sec:authorize>
        <sec:authorize access="hasRole('USER')">
        <tiles:insertAttribute name="gnb_user" />
        </sec:authorize>
        <sec:authorize access="hasRole('PARTNER')">
        <tiles:insertAttribute name="gnb_partner" />
        </sec:authorize>
        <tiles:insertAttribute name="body" />
        <tiles:insertAttribute name="footer" />
    </div>
    <tiles:insertAttribute name="modal" />
</body>

</html>

用戶登錄頁面
 

<%@ page contentType="text/html;charset=utf-8" pageEncoding="utf-8" session="false" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://tiles.apache.org/tags-tiles" prefix="tiles" %>
<div class="block-center login-wrapper">
<!-- START card-->
<div class="card card-flat login">
    <div class="card-header text-center">
        <a href="/">
            <img class="block-center rounded" src="/img/ci.png" alt="logo">
            <span class="login-tit">XXX系統</span>
        </a>
    </div>
    <div class="card-body">
        <form action="/login-process" method="post">
            <div class="form-group">
                <div class="input-group with-focus">
                    <input name="username" type="text" class="form-control border-right-0" placeholder="請輸入賬號" required autocomplete>
                    <div class="input-group-append">
                                <span class="input-group-text text-muted bg-transparent border-left-0">
                                    <em class="fa fa-user"></em>
                                </span>
                        </div>
                    </div>
                </div>
                <div class="form-group">
                    <div class="input-group with-focus">
                        <input name="password" class="form-control border-right-0" type="password" placeholder="請輸入密碼" required autocomplete>
                    <div class="input-group-append">
                            <span class="input-group-text text-muted bg-transparent border-left-0">
                                <em class="fa fa-lock"></em>
                            </span>
	                    </div>
	                </div>
	            </div>
            <button class="btn btn-block btn-primary mt-3 pop" type="submit">登錄 <span class="ml-1 icon-question"></span></button>
        </form>
        <input type="hidden" id="errCode" value="${errCode}" />
    </div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
	if ($("#errCode").val() != null && $("#errCode").val().length > 0) {
		switch($("#errCode").val()) {
		case "11002":
			$.notify("<strong>Warning!</strong> 未登錄的賬號", "warning");
			break;
		case "11003":
			$.notify("<strong>Warning!</strong> 賬號被鎖定", "warning");
			break;
		default:
			$.notify("<strong>Warning!</strong> 賬號密碼錯誤", "warning");
			break;
		}
	}
});
</script>

最後在需要驗證用戶權限的Controller中的類或方法中添加訪問權限的標籤來攔截在未登錄狀態下直接通過url訪問頁面

@PreAuthorize("hasRole('ROLE_ADMIN')")
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_PARTNER')")

以上是博主自己通過Spring Security實現的用戶登錄功能,博主僅僅只用了2天就把複雜的用戶登錄,根據權限顯示菜單等一系列功能開發完了。事實證明spring security讓用戶登錄功能很簡單的就實現了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章