利用springsecurity 自帶的SEESION做的登陸,JWT TOKEN也可以實現,但是需要配置過期處理,至於怎麼做是仁者見仁智者見智的事。
首先建立用戶 角色 權限,老司機們都知道不再詳述:
用戶對象:
/**
* 用戶對象
*
*/
@Entity
@Table(name="USER")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class User extends IdEntity {
private static final long serialVersionUID = -6998691082059319752L;
protected String userName;//用戶名
protected String empName;//員工英文名
protected String password;//密碼
protected Date createDate;//創建時間
protected Date lastLoginDate;//最後登錄時間
protected String phone;//手機號
protected Set<Role> roles = new HashSet<Role>();//擁有權限
public User() {
super();
}
public User(String userName, String empName, String password, Date createDate, Date lastLoginDate, String phone) {
super();
this.userName = userName;
this.empName = empName;
this.password = password;
this.createDate = createDate;
this.lastLoginDate = lastLoginDate;
this.phone = phone;
}
@Column(name="USER_NAME")
public String getUserName() {
return userName;
}
@Column(name="EMP_NAME")
public String getEmpName() {
return empName;
}
@Column(name="PASSWORD")
public String getPassword() {
return password;
}
@Column(name="CREATE_TIME")
public Date getCreateDate() {
return createDate;
}
@Column(name="LAST_LOGIN_TIME")
public Date getLastLoginDate() {
return lastLoginDate;
}
@ManyToMany(fetch=FetchType.LAZY, targetEntity=Role.class)
@JoinTable(name="USER_ROLE",
joinColumns = {@JoinColumn(name="USER_ID")},
inverseJoinColumns = {@JoinColumn(name="ROLE_ID")})
public Set<Role> getRoles() {
return roles;
}
@Column(name="PHONE")
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
public void setUserName(String userName) {
this.userName = userName;
}
public void setEmpName(String empName) {
this.empName = empName;
}
public void setPassword(String password) {
this.password = password;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public void setLastLoginDate(Date lastLoginDate) {
this.lastLoginDate = lastLoginDate;
}
@Override
public String toString() {
return "User [userName=" + userName + ", empName=" + empName + ", password=" + password + ", createDate="
+ createDate + ", lastLoginDate=" + lastLoginDate + ", phone=" + phone + ", roles=" + roles + "]";
}
}
角色對象:
/**
* 角色對象
*/
@Entity
@Table(name="ROLE")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Role extends IdEntity {
private static final long serialVersionUID = -6998691082059319752L;
protected String name;//角色名稱
protected String description;//角色描述
protected Set<Permission> permissions = new HashSet<Permission>();//擁有權限
protected Set<User> users = new HashSet<User>();//擁有角色的用戶
@Column(name="NAME")
public String getName() {
return name;
}
@Column(name="DESCRIPTION")
public String getDescription() {
return description;
}
@ManyToMany(fetch=FetchType.LAZY, targetEntity=Permission.class)
@JoinTable(name="ROLE_PERMISSION",
joinColumns = {@JoinColumn(name="ROLE_ID")},
inverseJoinColumns = {@JoinColumn(name="PERMISSION_ID")})
public Set<Permission> getPermissions() {
return permissions;
}
@JsonIgnore
@JSONField(serialize=false)
@ManyToMany(fetch=FetchType.LAZY, targetEntity=User.class)
@JoinTable(name="USER_ROLE", joinColumns =
@JoinColumn(name="ROLE_ID", referencedColumnName="ID"),
inverseJoinColumns=@JoinColumn(name="USER_ID", referencedColumnName="ID"))
public Set<User> getUsers() {
return users;
}
public void setUsers(Set<User> users) {
this.users = users;
}
public void setPermissions(Set<Permission> permissions) {
this.permissions = permissions;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Role [name=" + name + ", description=" + description + ", permissions=" + permissions + ", id=" + id + "]";
}
權限對象:
/**
* 角色對象
*/
@Entity
@Table(name="PERMISSION")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Permission extends IdEntity {
private static final long serialVersionUID = -6998691082059319752L;
protected String name;//權限名稱
protected String description;//權限描述
protected Permission parent;//所屬父
protected Set<Permission> children = new HashSet<Permission>();//擁有子權限
protected Set<Role> roles = new HashSet<Role>();//對應角色
protected PermissionsType type; //權限類型
protected String url; //權限的Api地址
@Column(name="NAME")
public String getName() {
return name;
}
@Column(name="DESCRIPTION")
public String getDescription() {
return description;
}
@ManyToOne(fetch=FetchType.LAZY, targetEntity=Permission.class)
@JoinColumn(name="PARENT_ID")
public Permission getParent() {
return parent;
}
@OneToMany(fetch=FetchType.LAZY, targetEntity=Permission.class, cascade=CascadeType.ALL, mappedBy="parent")
public Set<Permission> getChildren() {
return children;
}
@Column(name="TYPE")
@Convert(converter = PermissionsTypeConverter.class)
public PermissionsType getType() {
return type;
}
@JsonIgnore
@JSONField(serialize=false)
@ManyToMany(fetch=FetchType.LAZY, targetEntity=Role.class)
@JoinTable(name="ROLE_PERMISSION", joinColumns =
@JoinColumn(name="PERMISSION_ID"),
inverseJoinColumns=@JoinColumn(name="ROLE_ID"))
public Set<Role> getRoles() {
return roles;
}
@Column(name="URL")
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
public void setType(PermissionsType type) {
this.type = type;
}
public void setParent(Permission parent) {
this.parent = parent;
}
public void setChildren(Set<Permission> children) {
this.children = children;
}
public void setName(String name) {
this.name = name;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public String toString() {
return "Permission [name=" + name + ", description=" + description + ", parent=" + parent + ", children="
+ children + ", id=" + id + "]";
}
}
其中權限類型主要是區分是否需要設置查詢權限(根據自己的業務設置是否需要)
public enum PermissionsType {
ITEM (1, "模塊項"),
PERMISSION (2, "單個權限");
private Integer id;
private String name;
private PermissionsType(){
}
private PermissionsType(Integer id, String name){
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Integer id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public static PermissionsType getById (Integer id){
for (PermissionsType type : PermissionsType.values()) {
if(type.getId() == id){
return type;
}
}
return null;
}
}
對應的數據SQL:
#用戶表
DROP TABLE IF EXISTS USER;
CREATE TABLE USER (
ID INT(11) PRIMARY KEY NOT NULL AUTO_INCREMENT,
USER_NAME VARCHAR(500),
PASSWORD VARCHAR(500),
EMP_NAME VARCHAR(500),
CREATE_TIME DATETIME,
LAST_LOGIN_TIME DATETIME
);
ALTER TABLE user ADD COLUMN PHONE VARCHAR(200);
#角色表
DROP TABLE IF EXISTS ROLE;
CREATE TABLE customize.ROLE (
ID INT (11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR (20) NOT NULL,
DESCRIPTION VARCHAR (500),
PRIMARY KEY (ID)
)ENGINE = INNODB DEFAULT CHARSET = utf8;
#權限表
DROP TABLE IF EXISTS PERMISSION;
CREATE TABLE customize.PERMISSION (
ID INT (11) NOT NULL AUTO_INCREMENT,
NAME VARCHAR (100) NOT NULL,
DESCRIPTION VARCHAR (500),
PARENT_ID INT (11),
TYPE int(1),
PRIMARY KEY (ID)
)ENGINE = INNODB DEFAULT CHARSET = utf8;
ALTER TABLE PERMISSION ADD COLUMN URL VARCHAR(500);
#用戶角色關係表
DROP TABLE IF EXISTS USER_ROLE;
CREATE TABLE customize.USER_ROLE (
USER_ID INT (11) NOT NULL,
ROLE_ID INT (11) NOT NULL
)ENGINE = INNODB DEFAULT CHARSET = utf8;
#角色權限關係表
DROP TABLE IF EXISTS ROLE_PERMISSION;
CREATE TABLE customize.ROLE_PERMISSION (
ROLE_ID INT (11) NOT NULL,
PERMISSION_ID INT (11) NOT NULL
)ENGINE = INNODB DEFAULT CHARSET = utf8;
等一切準備完畢開始建立SPRING SECURTY的配置:
MAVEN 增加依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
增加對應的FILTER主要作用是預檢查全部返回200:
@Order(1)
@WebFilter(filterName = "myWebFilter", urlPatterns="/*")
public class MyWebFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if("OPTIONS".equalsIgnoreCase(request.getMethod())){
response.setStatus(HttpServletResponse.SC_OK);
return;
}
filterChain.doFilter(request, response);
}
}
增加對應的跨域配置:
/**
* 前後端分離跨域配置
*
*/
@Component
@Order(0)
public class CorsConfig extends CorsFilter {
public CorsConfig() {
super(configurationSource());
}
private static UrlBasedCorsConfigurationSource configurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 設置你要允許的網站域名,如果全允許則設爲 *
config.addAllowedOrigin("http://xxx.xxx.com");
// 如果要限制 HEADER 或 METHOD 請自行更改
config.addAllowedHeader("*");
config.addAllowedMethod("*");
// FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<CorsFilter>(new CorsFilter(source));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
增加自定義驗證參數類
/**
* 接受驗證的參數 目前只有賬號密碼 如果有驗證碼需要重新加參數
*
*/
public class MyWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 6975601077710753878L;
private String username;
private String password;
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;
}
public MyWebAuthenticationDetails(HttpServletRequest request) {
super(request);
username = request.getParameter("username");
password = request.getParameter("password");
}
使自定義的參數起作用:
/**
* 配置自定義的驗證參數
*/
@Component
public class MyAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new MyWebAuthenticationDetails(context);
}
}
WebSecurityConfig的核心配置:
@Configuration//配置文件註解
@EnableWebSecurity//禁用Boot的默認Security配置,配合@Configuration啓用自定義配置(需要擴展WebSecurityConfigurerAdapter)
@EnableGlobalMethodSecurity(prePostEnabled = true)//啓用Security註解,例如最常用的@PreAuthorize 本項目用的 RBAC此註解暫時不用
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
protected MyUserDetailsService myDetailService;
/**
* 身份驗證配置,用於注入自定義身份驗證Bean和密碼校驗規則
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myDetailService).passwordEncoder(new BCryptPasswordEncoder());
}
/**
* Request層面的配置,對應XML Configuration中的<http>元素
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().authenticationEntryPoint(new UnauthorizedEntryPoint()) //定義出錯後的異常信息
.and()
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS).permitAll() //所有的預檢查默認通過
.antMatchers("/user/info").permitAll() //登陸方法默認通過
.antMatchers("/user/info/").permitAll() //登陸方法默認通過
.antMatchers("/**").access("@rbacService.hasPermission(request,authentication)") //其餘任何方法需要RBAC過濾URL
.anyRequest().authenticated() //其餘需要權限驗證(因爲RBAC過濾全局,其實此句已無意義)
.and()
.headers()
.frameOptions() //預防iframe跨域問題
.disable()
.and()
.formLogin().loginPage("http://customize.joytrav.com/login")//設置的登陸頁面 默認 /login
.and()
.csrf().disable() //禁用CSRF驗證
.formLogin().successHandler(new AjaxAuthSuccessHandler()) //自定義登陸成功返回數據,否則會默認跳轉成功頁面
.failureHandler(new AjaxAuthFailHandler()) //自定義登陸失敗返回數據,否則會默認跳轉ERROR頁面
.loginProcessingUrl("/user/login/") //設置登陸的方法
.and()
.logout().logoutSuccessHandler(new AjaxLogoutSuccessHandler()) //自定義退出登陸返回數據,否則會默認跳轉登陸頁面
.logoutUrl("/logout"); //自定義退出登陸URL (方法中設置銷燬SESSION等邏輯)
http.addFilterBefore(new MyWebFilter(), UsernamePasswordAuthenticationFilter.class);//將常用的WEB FILTER 在用戶密碼驗證之前執行
}
//定義登陸成功返回信息
private class AjaxAuthSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpStatus.OK.value());
PrintWriter out = response.getWriter();
out.write("{\"status\":\"ok\",\"msg\":\"登錄成功\"}");
out.flush();
out.close();
}
}
//定義登陸失敗返回信息
private class AjaxAuthFailHandler extends SimpleUrlAuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter out = response.getWriter();
out.write("{\"status\":\"error\",\"msg\":\"請檢查用戶名、密碼或驗證碼是否正確\"}");
out.flush();
out.close();
}
}
//定義異常返回信息
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpStatus.FORBIDDEN.value(), authException.getMessage());
}
}
//定義登出成功返回信息
private class AjaxLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpStatus.OK.value());
PrintWriter out = response.getWriter();
out.write("{\"status\":\"ok\",\"msg\":\"登出成功\"}");
out.flush();
out.close();
}
}
}
增加對應的RBAC的接口:
public interface RbacService {
boolean hasPermission(HttpServletRequest request, Authentication authentication);
}
增加RBAC的實現類:
/**
* 過濾所有的URL 如果是ADMIN 則通過權限驗證,否則根據 USER.ROLES.PERMIMMIOS中皮質擁有權限的URL和當前請求的URL做對接,相等則通過,否則403
*/
@Component("rbacService")
public class RbacServiceImpl implements RbacService {
@Autowired
protected PermissionService permissionService;
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public static Logger log = LoggerFactory.getLogger(RbacServiceImpl.class);
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
log.debug("=========hasPermission Authentication");
Object principe = authentication.getPrincipal();
if(principe instanceof UserDetails) {
//拿到用戶名後可以拿到用戶角色和用戶所有的權限
Collection<? extends GrantedAuthority> grantedAuthorities = ((UserDetails) principe).getAuthorities();
//如果角色是admin 則直接通行
for(GrantedAuthority grantedAuthority : grantedAuthorities){
if(grantedAuthority.getAuthority().equalsIgnoreCase("ADMIN")){
log.debug("hasPermission with isAdmin");
return true;
}
}
//讀取用戶所有的url
Set<String> urls = new HashSet<String>();
for(GrantedAuthority grantedAuthority : grantedAuthorities){
String roleName = grantedAuthority.getAuthority();
urls.addAll(permissionService.getPermissionUrlsByRoleName(roleName));
}
urls.removeAll(java.util.Collections.singleton(null));
for(String url : urls) {
if(antPathMatcher.match(url, request.getRequestURI())) {
log.debug("hasPermission with matchUrl" + request.getRequestURI());
return true;
}
}
}
log.debug("no Permission " + request.getRequestURI());
return false;
}
}
增加對應的查詢邏輯,因爲比較簡單,此處只寫一個作爲示例:
@Repository
public interface PermissionDao extends BaseJpaRepository<Permission, Integer> {
@Query("from Permission p left join p.roles roles where roles.name = :roleName") //不加nativeQuery應使用HQL
List<Permission> getPermissionsByRoleName(@Param("roleName") String roleName);
@Query("select p.url from Permission p left join p.roles roles where roles.name = :roleName") //不加nativeQuery應使用HQL
List<String> getPermissionUrlsByRoleName(@Param("roleName") String roleName);
}
對應的VUE調用 最核心的問題 就是一定用 FORM提交,否則永遠返回自帶的登錄頁面:
this.$axios.post(
'/user/login/',
fd,
config
)
.then(res => {
//請求成功後的處理函數
console.log("res.data" + JSON.stringify(res.data));
if(res.data != null && res.data != ""){
this.logining = false;
//如果返回成功則獲取userInfo
this.$axios.post(
'/user/info/'
)
.then(res => {
sessionStorage.setItem("auth-user", JSON.stringify(res.data));
console.log(" this.$router" + this.$router);
this.$router.push({path: '/index'});
}).catch(err => {//請求失敗後的處理函數
this.$alert('報錯了'+err);
console.log(err);
this.logining = false;
});
} else {
this.$alert('賬號或密碼錯誤', '提示', {
confirmButtonText: 'ok'
})}
this.logining = false;
}).catch(err => { //請求失敗後的處理函數
this.$alert('報錯了'+err);
console.log(err);
this.logining = false;
});
對應的APACHE配置 可以根據實際情況配置 NGINX 等:
<VirtualHost *:80>
ServerName api.xxxx.com
Header set Access-Control-Allow-Origin http://xxx.xxx.com
Header set Access-Control-Allow-Credentials true
Header set Access-Control-Allow-Headers content-type
ProxyRequests Off
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:7889/
ProxyPassReverse / http://127.0.0.1:7889/
</VirtualHost>