久闻Spring Security 很强大,一直没有机会再实际项目中用到。这次有幸独自负责一个项目的登录,权限,根据权限显示页面目录等功能的开发,再此将开发的核心代码记录一下,方便以后参考。
参考demo项目下载地址:https://github.com/xiaoyao880609/security_demo/
首先说明项目是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让用户登录功能很简单的就实现了。