SpringBoot集成Security,前后端分离的SecurityConfig配置
前后端分离下保持状态是个问题,但是我这里不涉及分布式,所以用不上JWT,JWT根据项目情况来决定是否使用.
Authentication对象会记录用户的状态,所以不用定义一个token,从Authentication中getAuthorities方法获取用户的状态- 如果使用JWT来保持状态的话,就在拦截器上对token进行解码判断就行
先贴上Model代码:
package com.jlau.schoollocationsystem.model;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Created by cxr1205628673 on 2020/3/16.
*/
@Document("user")
public class OrdinaryUser extends User implements UserDetails,Serializable{
@Id
private String id;
private String username;
private String password;
private List<Role> roles;
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> auth = new ArrayList<>();
for (Role role:roles) {
auth.add(new SimpleGrantedAuthority(role.getName()));
}
return auth;
}
@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 String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
下面是websecurityconfig代码继承adaptor...
package com.jlau.schoollocationsystem.configuration;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jlau.schoollocationsystem.service.UserService;
import com.jlau.schoollocationsystem.utils.ResponseMsg;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
/**
* Created by cxr1205628673 on 2020/3/16.
*/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private UserService userService;
@Bean
public PasswordEncoder getPasswordEncoder(){
//前后端分离要注入一个PasswordEncoder来给successHandler使用
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**")
.hasRole("ADMIN")
.antMatchers("/user/**")
.access("hasAnyRole('ADMIN','USER')")
.antMatchers("/db/**")
.hasRole("DBA")
.antMatchers("/register")
.permitAll()
.anyRequest()//其他请求登录后可访问
.authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/user/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
Map map = new HashMap();
map.put("authenties",authentication.getAuthorities());
map.put("name",authentication.getName());
ResponseMsg rsp = new ResponseMsg(200,"ok",map);
PrintWriter out = httpServletResponse.getWriter();
out.write(objectMapper.writeValueAsString(rsp));
out.flush();
out.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
ObjectMapper objectMapper = new ObjectMapper();
ResponseMsg rsp = new ResponseMsg(500,"fail",e.getMessage());
PrintWriter out = httpServletResponse.getWriter();
out.write(objectMapper.writeValueAsString(rsp));
out.flush();
out.close();
}
})
.permitAll()
.and()
.sessionManagement()
.invalidSessionUrl("/session/invalid")//session过期后跳转的url
.and()
.logout()
.logoutUrl("/logout")
.clearAuthentication(true)
.invalidateHttpSession(true)
.addLogoutHandler(new LogoutHandler() {
@Override
public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) {
//此处可做一些清除工作
}
})
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
ResponseMsg rsp = new ResponseMsg(200,"ok","登出成功");
PrintWriter out = httpServletResponse.getWriter();
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(rsp));
out.flush();
out.close();
}
})
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
PrintWriter out = httpServletResponse.getWriter();
ResponseMsg rsp = new ResponseMsg(401,"fail","权限不足");
ObjectMapper objectMapper = new ObjectMapper();
out.write(objectMapper.writeValueAsString(rsp));
out.flush();
out.close();
}
})
.and()
.csrf()
.disable()
.cors();
}
}
这里因为前后端分离,所以需要successHandler和failureHandler对用户登录成功和失败进行定制返回。
而如果权限不足需要accessDeniedHandler处理权限不足时的返回。logoutSucessHandler同理。
loginPage()方法是指定用户如果未登录,跳转到那个url上,我们就在Controller上映射上对应到处理即可(这里注意如果用户未登录,那么访问任何url都是跳转到loginPage()指定的url上,只有登录后才会有权限不足的跳转)
loginProcessingUrl()则是指定用户登录提交的的地址,usernameParameter()和passwordParameter()是指定用户登录表单中用户名和密码的name属性值(前端内容)...
permitAll()是指前一个antMatcher的地址未登录时可以访问,不受保护,antMatcher().hasRole(/**/)或者antMatcher().access(/**/)就是表示地址受保护,只有满足权限才能访问。不够权限就会用accessDeniedHandler()中的内容处理。
可能会有人问,那么Security怎么验证用户呢,一个配置类继承WebSecurityConfigurerAdapter类后重写方法:
@Autowired private UserService userService; @Bean public PasswordEncoder getPasswordEncoder(){ //前后端分离要注入一个PasswordEncoder来给successHandler使用 return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { super.configure(auth); auth.userDetailsService(userService); }
UserService:
package com.jlau.schoollocationsystem.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jlau.schoollocationsystem.model.OrdinaryUser;
import com.jlau.schoollocationsystem.model.Role;
import com.jlau.schoollocationsystem.model.WXUser;
import com.jlau.schoollocationsystem.repository.RoleRepsitory;
import com.jlau.schoollocationsystem.repository.UserRepository;
import com.jlau.schoollocationsystem.utils.HttpClient;
import com.jlau.schoollocationsystem.utils.ResponseMsg;
import com.jlau.schoollocationsystem.utils.WXUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Created by cxr1205628673 on 2020/3/10.
*/
@Service
public class UserService implements UserDetailsService{
@Qualifier(value = "wxUserRepository")
@Autowired
private UserRepository<WXUser> userRepository;
@Qualifier(value = "ordinaryUserRepository")
@Autowired
private UserRepository<OrdinaryUser> ordinaryUserUserRepository;
@Autowired
private RoleRepsitory roleRepsitory;
@Autowired
private PasswordEncoder coding;
@Autowired
ObjectMapper objectMapper;
@Value("${wx.appid}")
String appId;
@Value("${wx.secret}")
String secert;
public ResponseMsg userWXLogin(String code,String encryptedData,String iv) throws Exception,NullPointerException {
final String accessUrl = "https://api.weixin.qq.com/sns/jscode2session?" +
"appid=" + appId + "&secret=" + secert + "&js_code=" + code + "&grant_type=authorization_code";
String result = HttpClient.sendGet(accessUrl);
JsonNode authResult = null;
JsonNode userInfoResult = null;
authResult = objectMapper.readTree(result);
if(authResult.has("errcode")) {
throw new Exception("授权失败" + authResult.get("errmsg").asText());
}
String openId = authResult.get("openid").asText();
String sessionKey = authResult.get("session_key").asText();
JsonNode userInfo = WXUtils.getUserInfo(encryptedData,sessionKey,iv);
String nickName = userInfo.get("nickName").asText();
String gender = userInfo.get("gender").asText();
String province = userInfo.get("province").asText();
String avatarUrl = userInfo.get("avatarUrl").asText();
String city = userInfo.get("city").asText();
WXUser user = new WXUser();
user.setAvatarUrl(avatarUrl);
user.setGender(Integer.parseInt(gender));
user.setLanguage(province + city);
user.setNickName(nickName);
user.setOpenId(openId);
user.setLoginTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
userRepository.addUser(user);
return new ResponseMsg(200, "ok", result);
}
public ResponseMsg ordinaryUserLogin(String username,String password) throws Exception{
String encrypPassword = coding.encode(password);
OrdinaryUser ouser = ordinaryUserUserRepository.findOneUser(username);
if(ouser != null && ouser.getUsername().equals(username) && ouser.getPassword().equals(encrypPassword)) {
return new ResponseMsg(200,"ok","登录成功");
}else {
throw new Exception("用户名或密码不正确");
}
}
public ResponseMsg ordinaryUserRegister(String username,String password) throws Exception{
String encrypPassword = coding.encode(password);
//DigestUtils.md5DigestAsHex(password.getBytes());
OrdinaryUser ouser = ordinaryUserUserRepository.findOneUser(username);
if(ouser != null){
throw new Exception("用户已经存在");
}
OrdinaryUser user = new OrdinaryUser();
user.setUsername(username);
user.setPassword(encrypPassword);
List roles = new ArrayList<>();
Role role = roleRepsitory.findRole("ROLE_USER");
roles.add(role);
user.setRoles(roles);
ordinaryUserUserRepository.addUser(user);
return new ResponseMsg(200,"ok","注册成功");
}
public ResponseMsg deleteOrdinaryUser(String id) {
ordinaryUserUserRepository.deleteUser(id);
return new ResponseMsg(200,"ok","删除成功");
}
public ResponseMsg updateOrdinaryUser(OrdinaryUser user) {
ordinaryUserUserRepository.updateUser(user);
return new ResponseMsg(200,"ok","修改成功");
}
public ResponseMsg findUser(String id) {
ordinaryUserUserRepository.findOneUser(id);
return new ResponseMsg(200,"ok","ok");
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
OrdinaryUser user = ordinaryUserUserRepository.findOneUser(username);
if(user == null) {
throw new UsernameNotFoundException("该账号不存在");
}
return user;
}
}
该类需要继承UserDetailsService,重写loadUserByUsername(String)方法,该方法实现根据传入的用户名来查询用户。
这也是Security获取用户的地方。而判断用户名和密码是否匹配则是在更深的地方,这就需要阅读源代码了。