前言
用户登录系统,最简单的解释是将用户账号和密码传输到后端,后端将传过来的账号和密码信息与数据库进行比对,如果正确则登陆成功。这一简单的描述可以概况绝大部分用户登录系统,但是真正实现的时候,我们不仅要考虑登录信息传递的安全性和稳定性,还要时刻确认用户的登录状态,而且用户的权限分离也很重要。其中用户信息的安全是首要的,也是每一个网站应做到的最基本的事情。
Spring Boot Security
框架原理
Spring Boot集成了Spring Security框架,Spring Security的主要核心功能为认证(Authentication)和授权(Authorization),所有的架构也是基于这两个核心功能去实现的。Spring Security在我们进行用户认证以及授予权限的时候,通过各种各样的拦截器来控制权限的访问,从而实现安全。
框架核心组件
- SecurityContextHolder:提供对SecurityContext的访问
- SecurityContext,:持有Authentication对象和其他可能需要的信息
- AuthenticationManager 其中可以包含多个AuthenticationProvider
- ProviderManager对象为AuthenticationManager接口的实现类
- AuthenticationProvider 主要用来进行认证操作的类 调用其中的authenticate()方法去进行认证操作
- Authentication:Spring Security方式的认证主体
- GrantedAuthority:对认证主题的应用层面的授权,含当前用户的权限信息,通常使用角色表示
- UserDetails:构建Authentication对象必须的信息,可以自定义,可能需要访问DB得到
- UserDetailsService:通过username构建UserDetails对象,通过loadUserByUsername根据userName获取UserDetail对象 (可以在这里基于自身业务进行自定义的实现 如通过数据库,xml,缓存获取等)
框架搭建
自定义了一个springSecurity安全框架的配置类继承WebSecurityConfigurerAdapter,重写其中的方法configure。实现该类后可以发现,在web容器启动的过程中该类实例对象会被WebSecurityConfiguration类处理。
SecurityConfig
package com.manager.system.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
SecurityUserService myUserService;
@Autowired
AuthenticationProviderCustom authenticationProviderCustom;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.authenticationProvider(authenticationProviderCustom);
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable().authorizeRequests()
.antMatchers("/admin/**").authenticated()
.antMatchers("/user/**").hasAuthority("SUPER_ADMIN")
.antMatchers("/static/**", "/img/**", "/picture/**", "/logo/**", "/auth/**").permitAll()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(false).sessionRegistry(sessionRegistry());
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/auth/**", "/picture/**", "/services/**", "/logo/**");
}
}
自定义一个SecurityUser类,继承自UserDetails。这个用户类在UserDetails的基础上添加我们数据库设计中用户表的元素,以满足登陆系统设计的需求。
SecurityUser
package com.manager.system.config.security;
import com.manager.system.model.ManagerUserView;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
public class SecurityUser implements UserDetails {
private static final long serialVersionUID = 1L;
private int pkid;
private String username;
private String password;
private int userStatus;
private int groupStatus;
private int groupPkid;
private int type;
private String groupName;
private List<Integer> levelAuthorities;
private List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
SecurityUser(ManagerUserView userView, List<Integer> levelAuthorities) {
super();
this.pkid = userView.getUserPkid();
this.username = userView.getUsername();
this.password = userView.getPassword();
this.setUserStatus(userView.getUserStatus());
this.setGroupStatus(userView.getGroupStatus());
this.setGroupPkid(userView.getGroupPkid());
this.setGroupName(userView.getGroupName());
this.type = userView.getType();
this.levelAuthorities = levelAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return AUTHORITIES;
}
public void setAuthorities(List<GrantedAuthority> AUTHORITIES) {
this.AUTHORITIES = AUTHORITIES;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@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 void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public int getPkid() {
return pkid;
}
public void setPkid(int pkid) {
this.pkid = pkid;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public List<Integer> getLevelAuthorities() {
return levelAuthorities;
}
public void setLevelAuthorities(List<Integer> levelAuthorities) {
this.levelAuthorities = levelAuthorities;
}
public int getUserStatus() {
return userStatus;
}
public void setUserStatus(int userStatus) {
this.userStatus = userStatus;
}
public int getGroupStatus() {
return groupStatus;
}
public void setGroupStatus(int groupStatus) {
this.groupStatus = groupStatus;
}
public int getGroupPkid() {
return groupPkid;
}
public void setGroupPkid(int groupPkid) {
this.groupPkid = groupPkid;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
}
自定义一个SecurityUserService类,继承自UserDetailsService。这个Service类根据SecurityUser类自定义编写本网站的登录成功之后的权限识别。如果这个用户名是“Simon”则自动创建一个超级管理员Simon(这其实是用户登录系统初始化的后门,一般只会在测试阶段触发)。否则,检验这个成功登录的用户信息,识别它的用户权限并写入AUTHORITIES。
SecurityUserService
package com.manager.system.config.security;
import com.manager.system.dao.ManagerUserViewMapper;
import com.manager.system.model.ManagerUserView;
import com.manager.system.model.ManagerUserViewExample;
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.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class SecurityUserService implements UserDetailsService {
@Autowired
public ManagerUserViewMapper managerUserViewMapper;
public SecurityUserService() {
}
@Override
public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException {
ManagerUserViewExample userViewExample = new ManagerUserViewExample();
List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>();
if (username.equals("simon")) {
AUTHORITIES.add(new SimpleGrantedAuthority("SUPER_ADMIN"));
userViewExample.createCriteria();
} else {
userViewExample.createCriteria().andUsernameEqualTo(username);
}
List<ManagerUserView> userList = managerUserViewMapper.selectByExample(userViewExample);
ManagerUserView userView = new ManagerUserView();
if (!userList.isEmpty()) {
userView = userList.get(0);
if (userView.getGroupPkid() == null) {
userView.setGroupPkid(-1);
}
if (userView.getGroupName() == null) {
userView.setGroupName("");
}
if (userView.getGroupStatus() == null) {
userView.setGroupStatus(-1);
}
if (userView.getType() == null) {
userView.setType(-1);
}
List<Integer> levelAuthorities = new ArrayList<Integer>();
for (int i = 0; i < userList.size(); i++) {
if (userList.get(i).getAuthorityPkid() != null) {
levelAuthorities.add(userList.get(i).getAuthorityPkid());
}
}
SecurityUser myuser = null;
if(username.equals("simon")){
userView.setUsername("simon");
userView.setPassword("zy");
userView.setUserStatus(1);
userView.setGroupStatus(1);
userView.setType(0);
myuser = new SecurityUser(userView, levelAuthorities);
}else{
if (userView.getUserStatus() == 1) {
if (userView.getGroupStatus() == 1) {
if (userView.getType() == 0) {
AUTHORITIES.add(new SimpleGrantedAuthority("SUPER_ADMIN"));
} else if (userView.getType() == 1) {
AUTHORITIES.add(new SimpleGrantedAuthority("ADMIN"));
} else {
AUTHORITIES.add(new SimpleGrantedAuthority("USER"));
}
}
}
myuser = new SecurityUser(userView, levelAuthorities);
}
myuser.setAuthorities(AUTHORITIES);
return myuser;
}
return null;
}
}
如此一来,我们成功实现了Spring Boot Security初始化步骤。就目前来说,我们已经完成了一个可以登录和注销的登录系统。但是,这个登录系统没有状态保持检测,也没有信息加密,属于一个非常不安全的用户登录系统。
因此,我们要先实现它为其他网页提供的用户认证功能,即检验用户是否是已登录状态,在这里我们采用的是JWT 认证模式。
JWT用户认证
为什么使用JWT认证
前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。
但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。
在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。
httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容,因为 cookie 默认被发了出去。
为了解决这些问题,我们可以使用加密算法对token进行加密,因为对称加密算法很容易被破解,所以我们使用这个非对称加密的JWT。
JWT文件配置
首先写一个Filter拦截器,当用户访问需要权限的网站时,拦截器会将请求拦截,然后根据这个用户的token信息判断他是否拥有访问该网页的权限。这个token的读取方式写在getJwtFromRequest函数中。
JwtAuthenticationFilter
package com.manager.system.config.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider tokenProvider;
@Autowired
private SecurityUserService securityUserService;
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUserNameFromJWT(jwt);
UserDetails userDetails = securityUserService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
}
} catch (Exception ex) {
logger.error("Could not set user authentication in security context", ex);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7, bearerToken.length());
}
return null;
}
}
这个是JWT的token生成文件,在build中,我们将用户名、系统时间、过期时间和签发者等信息生成token对象。同时,也提供了token有效性检验函数。
JwtTokenProvider
package com.manager.system.config.security;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);
@Value("${app.jwtSecret}")
private String jwtSecret;
@Value("${app.jwtExpirationInMs}")
private int jwtExpirationInMs;
public String generateToken(Authentication authentication) {
SecurityUser userPrincipal = (SecurityUser) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJWT(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException ex) {
logger.error("Invalid JWT signature");
} catch (MalformedJwtException ex) {
logger.error("Invalid JWT token");
} catch (ExpiredJwtException ex) {
logger.error("Expired JWT token");
} catch (UnsupportedJwtException ex) {
logger.error("Unsupported JWT token");
} catch (IllegalArgumentException ex) {
logger.error("JWT claims string is empty.");
}
return false;
}
}
根据JWT的标准,我们创建一个JwtAuthenticationResponse类,在里面生成标准的accessToken字符串。前端根据这个类的格式生成标准的Authentication对象,传递给后端用以检验用户信息。
JwtAuthenticationResponse
package com.manager.system.config.security;
public class JwtAuthenticationResponse {
private String accessToken;
private String tokenType = "Bearer";
private String currentAuthority;
public JwtAuthenticationResponse(String accessToken, String authority) {
this.accessToken = accessToken;
this.currentAuthority = authority;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getCurrentAuthority() {
return currentAuthority;
}
public void setCurrentAuthority(String authority) {
this.currentAuthority = authority;
}
}
自定义一个JwtAuthenticationEntryPoint类继承自AuthenticationEntryPoint,当访问发生错误时,会发送对应类型的网络错误给前端网页。
JwtAuthenticationEntryPoint
package com.manager.system.config.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
@Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
logger.error("Responding with unauthorized error. Message - {}", e.getMessage());
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"Sorry, You're not authorized to access this resource.");
}
}
对于前端传送过来的Header,我们自定义一个ExceptionHandler类来处理这个Header。
ExceptionHandler
package com.manager.system.config.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class ExceptionHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException deniedexception)
throws IOException, ServletException {
response.sendRedirect(request.getContextPath()+"/login");
}
}
上面我们检验的token是由后端在用户成功登录之后生成,并传送给前端的。在这里,我们需要自定义一个ApiResponse类对象,这个对象在用户成功登录之后生活,然后转化成json数据流传输给前端。
ApiResponse
package com.manager.system.config.security;
public class ApiResponse {
private int status;
private String statusText;
private String currentAuthority;
public ApiResponse(int code, String statusText, String currentAuthority) {
this.status = code;
this.statusText = statusText;
this.currentAuthority = currentAuthority;
}
public int getStatus() {
return status;
}
public void setSuccess(int status) {
this.status = status;
}
public String getStatusText() {
return statusText;
}
public void setStatusText(String statusText) {
this.statusText = statusText;
}
public String getCurrentAuthority() {
return currentAuthority;
}
public void setCurrentAuthority(String currentAuthority) {
this.currentAuthority = currentAuthority;
}
}
最后,我们要有一个用户登录检验接口,检测该用户的账号密码是否正确,状态是否为未注销,最后再根据用户组ID识别该用户的权限等级。出于用户信息安全考虑,我们在用户注册账号时,会对其密码使用BCrypt进行加密存储。于是,在检验用户账号密码时,要对密码进行BCrypt加密才能和数据库信息进行比对。
AuthenticationProviderCustom
package com.manager.system.config.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
@Component
public class AuthenticationProviderCustom implements AuthenticationProvider {
@Autowired
private SecurityUserService myUserService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = null;
SecurityUser user = null;
try {
username = new String(authentication.getName().getBytes("iso8859-1"), "utf-8");
System.out.println(username);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (username != null) {
user = (SecurityUser) myUserService.loadUserByUsername(username);
}
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
if (user != null) {
if (encoder.matches((CharSequence) authentication.getCredentials(), user.getPassword())) {
if (user.getUserStatus() == 1) {
if (user.getGroupStatus() == 1) {
return new UsernamePasswordAuthenticationToken(user, authentication.getCredentials(), user.getAuthorities());
}
throw new BadCredentialsException("Group disabled");
}
throw new BadCredentialsException("Account disabled");
}
throw new BadCredentialsException("Password error");
}
throw new BadCredentialsException("Username error");
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.equals(authentication);
}
}
结语
经过上面复杂的步骤,我们终于完成了JWT认证和BCrypt加密的用户登录系统,在用户登录经过层层加密和检验之后,他也终于可以访问我们的数据库啦!虽然无论后端多么复杂,用户还是只用输入账号和密码,但为了用户信息安全,再复杂的检验系统也做下去。信息安全永远是第一要义!
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)