最近项目上运用到了spring security来控制控制权限,然而钉钉端采用的前后端分离的模式进行开发,pc端未采用前后端分离。这样就出现了两个问题:
1.钉钉前端无法携带JSSESSIONID,从而导致spring security框架无法自动解析权限,需要自行实现token并存入redis。
2.钉钉端需要使用手机号码免密登录,pc端需要账号密码进行登录。
功能完成后,此处做个总结。
在SecurityConfig类中注册认证提供者
pc端我采用的是spring security默认的DaoAuthenticationProvider 从数据库中获取账号密码进行比对登录,将userDetailService的实现类改为了自己业务中的实现类。
钉钉端采用的是自行维护相关验证逻辑。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private HttpStatusEntryPoint httpStatusEntryPoint = new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
/**
* 成功的处理器
*/
@Autowired
private CsoftAuthenticationSuccessHandler csoftAuthenticationSuccessHandler;
@Autowired
private CsoftUserDetailService csoftUserDetailService;
@Autowired
private TokenAuthenticationProvider tokenAuthenticationProvider;
/**
* 将认证器注册到认证管理器中
* @param auth
* @throws Exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//此处将认证提供者注册到管理中心中统一管理
auth.authenticationProvider(tokenAuthenticationProvider)//自实现token的手机号码登录校验
.authenticationProvider(daoAuthenticationProvider())//springsecurity 自带的账号密码登录校验
;
}
/**
* 失败的处理器
*/
@Autowired
private CsoftAuthenticationFailureHandler csoftAuthenticationFailureHandler;
/**
* 注入密码加密对象
* @return Spring原生的加密对象Bean
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 将默认的认证中心中的属性 改为业务中的实现类
* @return
*/
@Bean
DaoAuthenticationProvider daoAuthenticationProvider(){
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
daoAuthenticationProvider.setUserDetailsService(csoftUserDetailService);
return daoAuthenticationProvider;
}
/**
* 验证码拦截器
*/
@Autowired
private ValidateCodeFilter validateCodeFilter;
//注册自定义的UsernamePasswordAuthenticationFilter
@Bean
CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
CustomAuthenticationFilter filter = new CustomAuthenticationFilter();
filter.setFilterProcessesUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM);
//这句很关键,重用WebSecurityConfigurerAdapter配置的AuthenticationManager,不然要自己组装AuthenticationManager
filter.setAuthenticationManager(authenticationManagerBean());
filter.setAuthenticationSuccessHandler(csoftAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(csoftAuthenticationFailureHandler);
return filter;
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 请求安全策略
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 跨域攻击防护
http.cors().and().csrf().disable();
// 同源打开iframe
http.headers().frameOptions().sameOrigin();
/**
* 通过表单用户名和密码登入验证
*/
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.formLogin().loginPage(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL)
.loginProcessingUrl(SecurityConstants.DEFAULT_SIGN_IN_PROCESSING_URL_FORM)
.successHandler(csoftAuthenticationSuccessHandler)
.failureHandler(csoftAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers("/account/**").hasAnyAuthority("ROLE_USER")
.antMatchers("/admin/**").hasAnyAuthority("ROLE_ADMIN")
.antMatchers(SecurityConstants.DEFAULT_UNAUTHENTICATION_URL).permitAll()
.anyRequest().authenticated();
//钉钉端过滤器
AuthenticationFilter filter = new AuthenticationFilter(authenticationManager(), httpStatusEntryPoint);
http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class);
}
一、手机号码免密登录 通过json格式访问在header中添加Token 访问后台,后台予以解析
第一步:实现AuthenticationProvider
@Component
public class TokenAuthenticationProvider implements AuthenticationProvider {
private static Logger logger = LoggerFactory.getLogger(TokenAuthenticationProvider.class);
@Autowired
private AuthTokenService authTokenService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String tokenId = authentication.getName();
logger.info(
"TokenAuthenticationProvider.class: authenticate() - tokenId:" + tokenId );
UserToken token = null;
try {
token = authTokenService.loadTokenById(tokenId);
} catch (UsernameNotFoundException e) {
throw new BadCredentialsException("Username not found.");
}
if (token == null) {
throw new BadCredentialsException("Username not found.");
}
Collection<? extends GrantedAuthority> authorities = token.getAuthorities();
return new MobileAuthenticationToken(token, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
//将此处改为自己编写的MobileAuthenticationToken类,providerManager会遍历
securityconfig中注册的provider,根据此方法返回true或false来决定由哪个provider
去校验请求过来的authentication。
return MobileAuthenticationToken.class
.isAssignableFrom(authentication);
}
}
第二步:实现MobileAuthenticationToken
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
// ~ Instance fields
// ================================================================================================
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* This constructor can be safely used by any code that wishes to create a
* <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
* will return <code>false</code>.
*
*/
public MobileAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
*
* @param principal
* @param credentials
* @param authorities
*/
public MobileAuthenticationToken(Object principal,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true); // must use super, as we override
}
// ~ Methods
// ========================================================================================================
public Object getPrincipal() {
return this.principal;
}
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
@Override
public Object getCredentials() {
return null;
}
第三步:实现filter用于鉴权
public class AuthenticationFilter extends OncePerRequestFilter {
ObjectMapper objectMapper;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationEntryPoint authenticationEntryPoint;
private AuthenticationManager authenticationManager;
/**
* Token 后面 有个空格
*/
private final static String TOKEN_KEY = "Token ";
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private boolean ignoreFailure = false;
private String credentialsCharset = "UTF-8";
public AuthenticationFilter(AuthenticationManager authenticationManager,
AuthenticationEntryPoint authenticationEntryPoint) {
Assert.notNull(authenticationManager, "authenticationManager cannot be null");
Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
this.authenticationManager = authenticationManager;
this.authenticationEntryPoint = authenticationEntryPoint;
objectMapper = new ObjectMapper();
}
@Override
public void afterPropertiesSet() {
Assert.notNull(this.authenticationManager, "An AuthenticationManager is required");
if (!isIgnoreFailure()) {
Assert.notNull(this.authenticationEntryPoint, "An AuthenticationEntryPoint is required");
}
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = logger.isDebugEnabled();
try {
String header = request.getHeader("Authorization") == null?"":request.getHeader("Authorization").toString();
logger.info("header:"+header);
String uri = request.getRequestURI();
String token = "";
if(header.startsWith(TOKEN_KEY)) {
token = extractAndDecodeHeader(header, request);
}
if (debug) {
logger.debug("Token Authentication Authorization header found for token '" + token + "'");
}
if (authenticationIsRequired(token)) {
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(token);
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
Authentication authResult = authenticationManager.authenticate(authRequest);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
} catch (AuthenticationException failed) {
if (debug) {
logger.debug("Authentication request for failed: " + failed);
}
rememberMeServices.loginFail(request, response);
onUnsuccessfulAuthentication(request, response, failed);
if (ignoreFailure) {
chain.doFilter(request, response);
} else {
authenticationEntryPoint.commence(request, response, failed);
}
}
chain.doFilter(request, response);
}
private String extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
byte[] decoded = header.substring(6).getBytes("UTF-8");
String token = new String(decoded, getCredentialsCharset(request));
return token;
}
//判断是否需要登录
private boolean authenticationIsRequired(String token) {
Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication();
logger.info("existingAuth:"+existingAuth);
if (existingAuth == null || !existingAuth.isAuthenticated()) {
return true;
}
if (existingAuth instanceof MobileAuthenticationToken && !existingAuth.getName().equals(token)) {
return true;
}
if (existingAuth instanceof AnonymousAuthenticationToken) {
return true;
}
return false;
}
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
Authentication authResult) throws IOException {
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException {
logger.info("用户验证失败");
String uri = request.getRequestURI();
logger.info("uri:"+uri);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("验证失败,请重新登录!")));
}
protected AuthenticationEntryPoint getAuthenticationEntryPoint() {
return authenticationEntryPoint;
}
protected AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
protected boolean isIgnoreFailure() {
return ignoreFailure;
}
public void setAuthenticationDetailsSource(
AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
this.authenticationDetailsSource = authenticationDetailsSource;
}
public void setRememberMeServices(RememberMeServices rememberMeServices) {
Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
this.rememberMeServices = rememberMeServices;
}
public void setCredentialsCharset(String credentialsCharset) {
Assert.hasText(credentialsCharset, "credentialsCharset cannot be null or empty");
this.credentialsCharset = credentialsCharset;
}
protected String getCredentialsCharset(HttpServletRequest httpRequest) {
return credentialsCharset;
}
}
第四步:实现手机号码登陆的controller
@RestController
public class LoginController {
private static Logger logger = LoggerFactory.getLogger(LoginController.class);
@Autowired
private UserService userService;
@Autowired
private LoginService loginService;
@RequestMapping(value = "/login", method = RequestMethod.POST, consumes = { MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
@CrossOrigin(origins = "*")
@ResponseBody
public LoginResponse loginAction(@RequestBody LoginRequest req, BindingResult result) {
LoginResponse res = new LoginResponse();
String userName = req.getLoginName();
String password = req.getPassword();
if(password!=null && !password.equals("")) {
//进行加密处理
try {
password = PwdEncoder.encryptBASE64(password.getBytes()).trim();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(req.getMobile()!=null && !"".equals(req.getMobile())) {
User user = userService.getUserByMobile(req.getMobile());
if(user==null) {
res.setResponseCode(AUTH_FAIL);
res.setMessage("该手机号尚未开通权限,请联系管理员!");
return res;
}
userName = user.getUsername();
password = user.getPassword();
}
logger.info("password:"+password);
UserToken token = loginService.authentication(userName, password);
if (token != null) {
res = new LoginResponse(userName,token.getUserId(), token, token.getAuthorities());
res.setResponseCode(SUCCESS);
res.setMessage("Login success for " + req.getLoginName());
logger.info("logsuccess:"+userName+" Token:"+token.getId()+" userId:"+token.getUserId());
} else {
res.setResponseCode(AUTH_FAIL);
res.setMessage("登入信息有误!");
return res;
}
return res;
}
@RequestMapping(value = "/logout", method = RequestMethod.GET, produces = { MediaType.APPLICATION_JSON_VALUE })
@CrossOrigin(origins = "*")
public @ResponseBody LogoffResponse logoffAction() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
UserToken token = (UserToken) authentication.getPrincipal();
loginService.logoff(token.getId());
LogoffResponse res = new LogoffResponse(SUCCESS, "logoffSuccess");
logger.info("loginOff success:"+token.getId());
return res;
}
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public @ResponseBody ErrorResponse handleBusinessError(BusinessException e) {
ErrorResponse res = new ErrorResponse();
res.setErrors(e.getErrors());
res.setResponseCode(AUTH_FAIL);
return res;
}
}
二、账号密码登录:
第一步:PC端的表单验证过滤器
public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_UTF8_VALUE)
|| request.getContentType().equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE)) {
//use jackson to deserialize json
ObjectMapper mapper = new ObjectMapper();
UsernamePasswordAuthenticationToken authRequest = null;
try (InputStream is = request.getInputStream()) {
AuthenticationBean authenticationBean = mapper.readValue(is, AuthenticationBean.class);
if(authenticationBean!=null) {
authenticationBean = AuthenticationBean.decode(authenticationBean);
authRequest = new UsernamePasswordAuthenticationToken(
authenticationBean.getUsername(), authenticationBean.getPassword());
} catch (IOException e) {
e.printStackTrace();
authRequest = new UsernamePasswordAuthenticationToken(
"", "");
} finally {
setDetails(request, authRequest);
Authentication authResult = getAuthenticationManager().authenticate(authRequest);
return authResult;
}
}
else {
return super.attemptAuthentication(request, response);
}
}
}
第二步:自定义的userDetailServiceImpl
@Component
public class CsUserDetailService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Autowired
private UserRoleDao userRoleDao;
@Autowired
private RoleDao roleDao;
Logger log = LoggerFactory.getLogger(CsUserDetailService.class);
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 如何不正确的用户名直接返回不认证
if (username == null && username.isEmpty()) {
return null;
}
User user = userDao.getUserByName(username);
log.info("user:"+user);
if (user==null) {
throw new UsernameNotFoundException("不存在的用户");
}
// 查询出用户权限关联表
List<UserRole> userRoles = userRoleDao.findUserRolesByUserId(user.getId());
for (UserRole userRole : userRoles) {
user.getAuthorities().add(userRole );
}
return user;
}