在springboot项目中使用spring-security实现登陆验证,登陆成功则返回角色及权限信息,登陆失败则返回失败原因。
在实现此功能的时候,在网上找了大量教程,但是均没有符合我需求的,最后在参考一篇文档的基础上进行了代码实现。
使用restClient发送post请求,post体中携带了username和password等信息。后台会根据此账号查询数据库判断此用户是否存在,如果存在则进一步进行账号密码的验证,验证通过再去查询此账号拥有的角色及权限信息。
具体实现代码如下:
com.baomidou
mybatis-plus-boot-starter
${mybatis.plus.version}
com.alibaba
druid-spring-boot-starter
${druild.version}
com.alibaba
fastjson
${fastjson.version}
org.projectlombok
lombok
${lombok.version}
org.springframework.boot
spring-boot-starter-jdbc
${spring.boot.version}
mysql
mysql-connector-java
${mysql.version}
org.springframework.boot
spring-boot-starter-security
org.json
json
20160810
com.jayway.jsonpath
json-path
2.4.0
在上面除了引入了spring-security的依赖之外,还有三种json解析的依赖,都有不同的作用,还有一些其他依赖是连接数据库所用。
package com.mas.leadsscoring.process.vo;
import com.mas.leadsscoring.process.entity.TtRole;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* @Description spring-security的user对象
* @Author LiuYue
* @Date 2019/3/6
* @Version 1.0
*/
@Data
public class UserVO implements UserDetails {
/**
* 用户ID
*/
private String userId;
/**
* 用户账号
*/
private String user;
/**
* 用户名
*/
private String userName;
/**
* 密码
*/
private String password;
/**
* 是否为回收站,0表示不是,1表示是
*/
private String isRecycle;
/**
* 是否可用。Y表示可用,N表示不可用
*/
private String isEnable;
/**
* 创建人
*/
private String createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private String updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 备注
*/
private String comment;
/**
* 用户角色
**/
private List roles;
@Override
public Collection extends GrantedAuthority> getAuthorities() {
List authorities = new ArrayList<>();
for (TtRole role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRole()));
}
return authorities;
}
@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 "y".equals(isEnable) ;
}
}
User类实现了UserDetails接口,并定义了role属性,此属性是一个对象,对象中存放了角色的一些属性。并重写了getAuthorities()方法,在此方法中将角色信息加入到List中。
package com.mas.leadsscoring.process.service.impl;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.mas.leadsscoring.common.util.TransformUtils;
import com.mas.leadsscoring.process.entity.TtRole;
import com.mas.leadsscoring.process.entity.TtUser;
import com.mas.leadsscoring.process.mapper.TtRoleMapper;
import com.mas.leadsscoring.process.mapper.TtUserMapper;
import com.mas.leadsscoring.process.service.ITtUserService;
import com.mas.leadsscoring.common.base.service.impl.BaseServiceImpl;
import com.mas.leadsscoring.process.vo.UserVO;
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 javax.annotation.Resource;
import java.util.List;
/**
*
* 'leads_process.tt_table_config' is not BASE TABLE 服务实现类
*
*
* @author suntianchi
* @since 2019-03-05
*/
@Service("ttUserServiceImpl")
public class TtUserServiceImpl extends BaseServiceImpl implements ITtUserService {
private TtUserMapper ttUserMapper;
private TtRoleMapper ttRoleMapper;
@Autowired
public TtUserServiceImpl(TtUserMapper ttUserMapper, TtRoleMapper ttRoleMapper) {
this.ttUserMapper = ttUserMapper;
this.ttRoleMapper = ttRoleMapper;
}
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
TtUser ttUser = ttUserMapper.getUserByUsername(s);
if (ttUser == null) {
throw new UsernameNotFoundException("用户名不对");
}
List roles = ttRoleMapper.getUserRole(ttUser.getUserId());
//由于mybatis-plus会将TtUser对象中的属性封装到sql中进行查询,所以没有直接在TtUser对象中增加Role属性,而是通过TransformUtils中的beanToBean方法,将TtUser转换成实现UserDetail方法的UserVO对象。beanToBean方法会将相同的属性进行值传递,不同属性则不会变化。
UserVO userVO = TransformUtils.beanToBean(ttUser, UserVO.class);
userVO.setRoles(roles);
return userVO;
}
}
TtUserServiceImpl方法继承了BaseServiceImpl方法,此方法是mybatisPlus提供的方法,里面封装了一些基本的增删改查的方法。
TtUserServiceImpl还实现了ITtUserService接口,此接口继承了UserDetailsService接口,所以最后也就相当于TtUserServiceImpl实现了UserDetailsService接口。
public interface ITtUserService extends IBaseService, UserDetailsService {
@Override
UserDetails loadUserByUsername(String s) throws UsernameNotFoundException;
}
UserDetailsService接口是spring-security框架中提供的一个接口,实现此接口的loadUserByUsername方法,最后并返回查询到的用户信息,spring-security框架就会对此用户进行账号密码校验,这些会在待会的debug模式中一步一步展示。
package com.mas.leadsscoring.process.config.security;
import com.mas.leadsscoring.process.entity.RespBean;
import com.mas.leadsscoring.process.service.impl.TtUserServiceImpl;
import com.mas.leadsscoring.process.util.CommonUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* @Description 验证授权配置类
* @Author LiuYue
* @Date 2019/3/5
* @Version 1.0
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) //开启security注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomLoginHandler customLoginHandler;
@Autowired
private CustomLogoutHandler customLogoutHandler;
@Autowired
private CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
private TtUserServiceImpl ttUserService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(ttUserService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.GET).permitAll()
.antMatchers(HttpMethod.POST).permitAll()
.antMatchers("/api/**").permitAll();
http.formLogin();
http.logout().logoutUrl("/api/session/logout")
// 登出前调用,可用于日志
.addLogoutHandler(customLogoutHandler)
// 登出后调用,用户信息已不存在
.logoutSuccessHandler(customLogoutHandler);
http.exceptionHandling()
// 已登入用户的权限错误
.accessDeniedHandler(customAccessDeniedHandler)
// 未登入用户的权限错误
.authenticationEntryPoint(customAccessDeniedHandler);
http.csrf().disable();
http.addFilterAt(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
private static void responseText(HttpServletResponse response, String content) throws IOException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
response.setContentLength(bytes.length);
response.getOutputStream().write(bytes);
response.flushBuffer();
}
private CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setAuthenticationSuccessHandler(customLoginHandler);
filter.setAuthenticationFailureHandler(customLoginHandler);
filter.setAuthenticationManager(authenticationManager());
filter.setFilterProcessesUrl("/api/login");
return filter;
}
@Component
public static class CustomLoginHandler extends RespBean implements AuthenticationSuccessHandler, AuthenticationFailureHandler {
// Login Success
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
responseText(response, objectResult(CommonUtils.getJSON(authentication)));
}
// Login Failure
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
responseText(response, errorMessage(exception.getMessage()));
}
}
@Component
public static class CustomAccessDeniedHandler extends RespBean implements AuthenticationEntryPoint, AccessDeniedHandler {
// NoLogged Access Denied
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
responseText(response, errorMessage(authException.getMessage()));
}
// Logged Access Denied
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
responseText(response, errorMessage(accessDeniedException.getMessage()));
}
}
@Component
public static class CustomLogoutHandler extends RespBean implements LogoutHandler, LogoutSuccessHandler {
// Before Logout
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
}
// After Logout
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException {
responseText(response, objectResult(CommonUtils.getJSON(null)));
}
}
}
package com.mas.leadsscoring.process.config.security;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
/**
* @Description security登录过滤
* @Author LiuYue
* @Date 2019/3/6
* @Version 1.0
*/
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
UsernamePasswordAuthenticationToken authRequest;
try (InputStream is = request.getInputStream()) {
DocumentContext context = JsonPath.parse(is);
String username = context.read("$.username", String.class);
String password = context.read("$.password", String.class);
authRequest = new UsernamePasswordAuthenticationToken(username, password);
} catch (Exception e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken("", "");
}
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
在此过滤器中使用jsonpath获取request请求中的username和password信息,并将其配置到UsernamePasswordAuthenticationToken对象中,后面会根据此对象的密码与数据库查询到的密码进行比对。
package com.mas.leadsscoring.process.entity;
import org.json.JSONObject;
import org.springframework.http.MediaType;
/**
* @Description 后台返回前端对象
* @Author LiuYue
* @Date 2019/3/5
* @Version 1.0
*/
public class RespBean {
protected static final String MEDIA_TYPE = MediaType.APPLICATION_JSON_UTF8_VALUE;
/**
* @Description 返回成功
* @Author LiuYue
* @Date 2019/3/6
* @Param [object]
* @return java.lang.String
**/
protected String objectResult(Object object) {
JSONObject root = new JSONObject();
root.put("success", true);
root.put("data", object);
return root.toString();
}
/**
* @Description 返回失败
* @Author LiuYue
* @Date 2019/3/6
* @Param [message]
* @return java.lang.String
**/
protected String errorMessage(String message) {
JSONObject root = new JSONObject();
root.put("success", false);
root.put("message", message);
return root.toString();
}
}
此对象就是用作登陆失败与登陆成功封装返回信息内容。
在项目开始启动的时候spring-security会进行相应对象的初始化。此处进行继承WebSecurityConfigurerAdapter对象的初始化,可以看见进行了密码加密,以及对应请求过滤的配置。
根据此请求进行拦截,进入到我们配置的过滤器中:
如果用户存在,则会返回此用户的密码角色等信息。
由于UserVo实现了Userdetial接口,此方法中会根据Userdetial查询其对象中的信息,进行密码判断。
如果用户密码校验通过,则会获取角色信息