利用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 roles = new HashSet();//拥有权限
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 getRoles() {
return roles;
}
@Column(name="PHONE")
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public void setRoles(Set 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 permissions = new HashSet();//拥有权限
protected Set users = new HashSet();//拥有角色的用户
@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 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 getUsers() {
return users;
}
public void setUsers(Set users) {
this.users = users;
}
public void setPermissions(Set 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 children = new HashSet();//拥有子权限
protected Set roles = new HashSet();//对应角色
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 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 getRoles() {
return roles;
}
@Column(name="URL")
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public void setRoles(Set roles) {
this.roles = roles;
}
public void setType(PermissionsType type) {
this.type = type;
}
public void setParent(Permission parent) {
this.parent = parent;
}
public void setChildren(Set 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 增加依赖:
org.springframework.boot
spring-boot-starter-security
增加对应的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 bean = new FilterRegistrationBean(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 {
@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中的元素
*/
@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 urls = new HashSet();
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 {
@Query("from Permission p left join p.roles roles where roles.name = :roleName") //不加nativeQuery应使用HQL
List getPermissionsByRoleName(@Param("roleName") String roleName);
@Query("select p.url from Permission p left join p.roles roles where roles.name = :roleName") //不加nativeQuery应使用HQL
List 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 等:
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/