赞
踩
Spring Security是Spring提供的一个安全框架,提供认证和授权功能,最主要的是它提供了简单的使用方式,同时又有很高的灵活性,简单、灵活、强大
一般系统里关于角色方面通常有这么几张表:用户表、角色表、用户-角色表、菜单表、角色-菜单表、权限表、角色-权限表等
JWT认证的实现
(1)支持用户通过用户名和密码登录
(2)登录后通过http header返回token,每次请求,客户端需通过header将token带回,用于权限校验
(3)服务端负责token的定期刷新
<properties>
<jjwt.version>0.9.1</jjwt.version>
<spring-security-jwt.version>1.0.11.RELEASE</spring-security-jwt.version>
</properties>
<!-- Spring Security-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>${spring-security-jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,默认是关闭的
public class SecurityConfig extends WebSecurityConfigurerAdapter {
/**
* 自定义登录成功处理器
*/
@Autowired
private UserLoginSuccessHandler userLoginSuccessHandler;
/**
* 自定义登录失败处理器
*/
@Autowired
private UserLoginFailureHandler userLoginFailureHandler;
/**
* 自定义注销成功处理器
*/
@Autowired
private UserLogoutSuccessHandler userLogoutSuccessHandler;
/**
* 自定义暂无权限处理器
*/
@Autowired
private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
/**
* 自定义未登录的处理器
*/
@Autowired
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
/**
* 自定义登录逻辑验证器
*/
@Autowired
private UserAuthenticationProvider userAuthenticationProvider;
/**
* 加密方式
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 注入自定义PermissionEvaluator
*/
@Bean
public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler(){
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(new UserPermissionEvaluator());
return handler;
}
/**
* 配置登录验证逻辑
*/
@Override
protected void configure(AuthenticationManagerBuilder auth){
//启用自定义的登陆验证逻辑
auth.authenticationProvider(userAuthenticationProvider);
}
/**
* 配置security的控制逻辑
* @Param http 请求
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 不进行权限验证的请求或资源(从配置文件中读取)
.antMatchers(JWTConfig.antMatchers.split(",")).permitAll()
// .antMatchers("/*").permitAll()
// 其他的需要登陆后才能访问
.anyRequest().authenticated()
.and()
// 配置未登录自定义处理类
.httpBasic().authenticationEntryPoint(userAuthenticationEntryPointHandler)
.and()
// 配置登录地址
.formLogin()
.loginProcessingUrl("/login/userLogin")
// 配置登录成功自定义处理类
.successHandler(userLoginSuccessHandler)
// 配置登录失败自定义处理类
.failureHandler(userLoginFailureHandler)
.and()
// 配置登出地址
.logout()
.logoutUrl("/login/userLogout")
// 配置用户登出自定义处理类
.logoutSuccessHandler(userLogoutSuccessHandler)
.and()
// 配置没有权限自定义处理类
.exceptionHandling().accessDeniedHandler(userAuthAccessDeniedHandler)
.and()
// 开启跨域
.cors()
.and()
// 取消跨站请求伪造防护
.csrf().disable();
// 基于Token不需要session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 禁用缓存
http.headers().cacheControl();
// 添加JWT过滤器
http.addFilter(new JWTAuthenticationTokenFilter(authenticationManager()));
}
}
// JWT相关配置类
@Data
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtConfig {
/**
* 密钥KEY
*/
private String secret;
/**
* TokenKey
*/
private String tokenHeader;
/**
* Token前缀字符
*/
private String tokenPrefix;
/**
* 过期时间
*/
private Integer expiration;
/**
* 不需要认证的接口
*/
private String antMatchers;
/**
* jwt发行人
*/
private String issuer;
/**
* 主体
*/
private String subject;
/**
* 接受者
*/
private String audience;
/**
* 有效时间
*/
private String validTime;
}
application.yml
# JWT配置
jwt:
# 密匙KEY
secret: secret
# Header KEY
tokenHeader: infinite-auth
# Token前缀字符
tokenPrefix: authorization-
# 过期时间 单位秒 1天后过期=86400 7天后过期=604800
expiration: 86400
# 配置不需要认证的接口
antMatchers: /generator/table/index
# jwt发行人
issuer: infinite
# 主体
subject: to-infinite
# 接受者
audience: infinite-user
# 有效时间,分钟
validTime: 30
@Component
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
/**
* 登录成功返回结果
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
// 组装JWT
SelfUserEntity selfUserEntity = (SelfUserEntity) authentication.getPrincipal();
String token = JWTTokenUtil.createAccessToken(selfUserEntity);
token = JWTConfig.tokenPrefix + token;
// 封装返回参数
Map<String,Object> resultData = new HashMap<>();
resultData.put("code","200");
resultData.put("msg", "登录成功");
resultData.put("token",token);
ResultUtil.responseJson(response,resultData);
}
}
@Component
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
/**
* 登录失败返回结果
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
// 这些对于操作的处理类可以根据不同异常进行不同处理
if (exception instanceof UsernameNotFoundException){
System.out.println("【登录失败】"+exception.getMessage());
ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户名不存在"));
}
if (exception instanceof LockedException){
System.out.println("【登录失败】"+exception.getMessage());
ResultUtil.responseJson(response,ResultUtil.resultCode(500,"用户被冻结"));
}
if (exception instanceof BadCredentialsException){
System.out.println("【登录失败】"+exception.getMessage());
ResultUtil.responseJson(response,ResultUtil.resultCode(500,"密码错误"));
}
ResultUtil.responseJson(response,ResultUtil.resultCode(500,"登录失败"));
}
}
@Component
public class UserLogoutSuccessHandler implements LogoutSuccessHandler {
/**
* 用户登出返回结果,前端清除掉Token
*/
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
Map<String,Object> resultData = new HashMap<>();
resultData.put("code","200");
resultData.put("msg", "登出成功");
SecurityContextHolder.clearContext();
ResultUtil.responseJson(response,ResultUtil.resultSuccess(resultData));
}
}
@Component
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
/**
* 暂无权限返回结果
*/
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception){
ResultUtil.responseJson(response,ResultUtil.resultCode(403,"未授权"));
}
}
@Component
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
/**
* 用户未登录返回结果
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception){
ResultUtil.responseJson(response,ResultUtil.resultCode(401,"未登录"));
}
}
@Component
public class UserAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UsersService usersService;
@Autowired
private UsermetaService usermetaService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 获取表单输入中返回的用户名
String userName = (String) authentication.getPrincipal();
// 获取表单中输入的密码
String password = (String) authentication.getCredentials();
// 查询用户是否存在
SelfUserEntity userInfo = usersService.getUserInfo(userName);
if (userInfo.getUsername() == null || userInfo.getUsername() == "") {
throw new UsernameNotFoundException("用户名不存在");
}
// 判断密码是否正确,这里我们的密码使用BCryptPasswordEncoder进行加密的
if (!new BCryptPasswordEncoder().matches(password, userInfo.getPassword())) {
throw new BadCredentialsException("密码不正确");
}
// 还可以加一些其他信息的判断,比如用户账号已停用等判断
if (userInfo.getStatus().equals("1")) {
throw new LockedException("该用户已被冻结");
}
// 角色集合
Set<GrantedAuthority> authorities = new HashSet<>();
EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
roleWrapper.eq("user_id",userInfo.getUserId());
roleWrapper.eq("meta_key","wp_user_level");
// 查询用户角色
List<Usermeta> sysRoleEntityList = usermetaService.selectList(roleWrapper);
for (Usermeta sysRoleEntity: sysRoleEntityList){
authorities.add(new SimpleGrantedAuthority("ROLE_" + sysRoleEntity.getMetaValue()));
}
userInfo.setAuthorities(authorities);
// 进行登录
return new UsernamePasswordAuthenticationToken(userInfo, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return true;
}
}
@Component
public class UserPermissionEvaluator implements PermissionEvaluator {
@Autowired
private UsermetaService usermetaService;
/**
* hasPermission鉴权方法
* 这里仅仅判断PreAuthorize注解中的权限表达式
* 实际中可以根据业务需求设计数据库通过targetUrl和permission做更复杂鉴权
* 当然targetUrl不一定是URL可以是数据Id还可以是管理员标识等,这里根据需求自行设计
* @Param authentication 用户身份(在使用hasPermission表达式时Authentication参数默认会自动带上)
* @Param targetUrl 请求路径
* @Param permission 请求路径权限
* @Return boolean 是否通过
*/
@Override
public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
// 获取用户信息
Usermeta selfUserEntity =(Usermeta) authentication.getPrincipal();
// 查询用户权限(这里可以将权限放入缓存中提升效率)
Set<String> permissions = new HashSet<>();
EntityWrapper<Usermeta> roleWrapper = new EntityWrapper<>();
roleWrapper.eq("user_id",selfUserEntity.getUserId());
roleWrapper.eq("meta_key","wp_user_level");
List<Usermeta> sysMenuEntityList = usermetaService.selectList(roleWrapper);
for (Usermeta sysMenuEntity:sysMenuEntityList) {
permissions.add(sysMenuEntity.getMetaValue());
}
// 权限对比
if (permissions.contains(permission.toString())){
return true;
}
return true;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
return false;
}
}
Spring Security用户的实体,必须要实现UserDetails接口。
public class SelfUserEntity implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 状态
*/
private String status;
/**
* 显示名称
*/
private String displayName;
/**
* 用户参数
*/
private Map<String, String> userParamMap;
/**
* 用户角色
*/
private Collection<GrantedAuthority> authorities;
/**
* 账户是否过期
*/
private boolean isAccountNonExpired = false;
/**
* 账户是否被锁定
*/
private boolean isAccountNonLocked = false;
/**
* 证书是否过期
*/
private boolean isCredentialsNonExpired = false;
/**
* 账户是否有效
*/
private boolean isEnabled = true;
// 省略getter/setter
}
JWT接口请求校验拦截器,请求接口时会进入这里验证Token是否合法和过期
public class JWTAuthenticationTokenFilter extends BasicAuthenticationFilter {
public JWTAuthenticationTokenFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中JWT的Token
String tokenHeader = request.getHeader(JWTConfig.tokenHeader);
if (null != tokenHeader && tokenHeader.startsWith(JWTConfig.tokenPrefix)) {
try {
// 截取JWT前缀
String token = tokenHeader.replace(JWTConfig.tokenPrefix, "");
// 解析JWT
Claims claims = Jwts.parser()
.setSigningKey(JWTConfig.secret)
.parseClaimsJws(token)
.getBody();
// 获取用户名
String username = claims.getSubject();
String userId = claims.getId();
if (!StringUtils.isEmpty(username) && !StringUtils.isEmpty(userId)) {
// 获取角色
List<GrantedAuthority> authorities = new ArrayList<>();
String authority = claims.get("authorities").toString();
if (!StringUtils.isEmpty(authority)) {
List<Map<String, String>> authorityMap = JSONObject.parseObject(authority, List.class);
for (Map<String, String> role : authorityMap) {
if (!StringUtils.isEmpty(role)) {
authorities.add(new SimpleGrantedAuthority(role.get("authority")));
}
}
}
//组装参数
SelfUserEntity selfUserEntity = new SelfUserEntity();
selfUserEntity.setUsername(claims.getSubject());
selfUserEntity.setUserId(Long.parseLong(claims.getId()));
selfUserEntity.setAuthorities(authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(selfUserEntity, userId, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (ExpiredJwtException e) {
System.out.println("Token过期");
} catch (Exception e) {
System.out.println("Token无效");
}
}
filterChain.doFilter(request, response);
}
}
@Component
public class SelfUserDetailsService implements UserDetailsService {
@Autowired
private UsersService usersService;
/**
* 查询用户信息
*
* @Param username 用户名
* @Return UserDetails SpringSecurity用户信息
*/
@Override
public SelfUserEntity loadUserByUsername(String username) throws UsernameNotFoundException {
EntityWrapper<Users> wrapper = new EntityWrapper<>();
//邮箱正则表达式
String expr = "^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})$";
//是否为邮箱
if (username.matches(expr)) {
wrapper.eq("user_email", username);
} else {
wrapper.eq("user_login", username);
}
// 查询用户信息
Users sysUserEntity = usersService.selectOne(wrapper);
if (sysUserEntity != null) {
// 组装参数
SelfUserEntity selfUserEntity = new SelfUserEntity();
BeanUtils.copyProperties(sysUserEntity, selfUserEntity);
return selfUserEntity;
}
return null;
}
}
@Secured
当@EnableGlobalMethodSecurity(securedEnabled=true)的时候,@Secured可以使用。
@PostMapping("/helloUser")
@Secured({"ROLE_normal","ROLE_admin"})
public Map<String, Object> initDashboard() {
Map<String, Object> result = new HashMap<>();
result.put(ResponseDict.RESPONSE_TITLE_KEY, "仪表盘初始化");
result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
return ResultUtil.resultSuccess(result);
}
说明:拥有normal或者admin角色的用户都可以方法helloUser()方法。另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
@PreAuthorize
Spring的 @PreAuthorize/@PostAuthorize 注解更适合方法级的安全,也支持Spring 表达式语言,提供了基于表达式的访问控制。
当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PreAuthorize可以使用:
@PostMapping("/initDashboard")
@PreAuthorize("hasRole('100')")
public Map<String, Object> initDashboard() {
Map<String, Object> result = new HashMap<>();
result.put(ResponseDict.RESPONSE_TITLE_KEY, "仪表盘初始化");
result.put(ResponseDict.RESPONSE_DATA_KEY, dashboardService.initDashboard());
return ResultUtil.resultSuccess(result);
}
@PostAuthorize
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限,Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject。
当@EnableGlobalMethodSecurity(prePostEnabled=true)的时候,@PostAuthorize可以使用:
@GetMapping("/getUserInfo")
@PostAuthorize(" returnObject!=null && returnObject.username == authentication.name")
public User getUserInfo() {
Object pricipal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
User user;
if("anonymousUser".equals(pricipal)) {
user = null;
} else {
user = (User) pricipal;
}
return user;
}
(1)拿到正确token,正常登录
(2)请求中如果不携带token的话,请求其它接口就会显示没有登录的提示
(3)登录账户无权限
在UserDeitails接口里面有一个getAuthorities()方法。这个方法将返回此用户的所拥有的权限。这个集合将用于用户的访问控制,也就是Authorization
①权限:就是一个字符串。一般不会重复
②权限检查:就是查看用户权限列表中是否含有匹配的字符串
public interface GrantedAuthority extends Serializable {
// AccessDecisionManager访问控制决策,返回一个字符串
String getAuthority();
}
在Spring Security中,角色和权限共用GrantedAuthority接口,唯一的不同角色就是多了个前缀“ROLE_”,而且它没有Shiro的那种从属关系,即一个角色包含哪些权限等等。在Spring Security看来角色和权限时一样的,它认证的时候,把所有权限(角色、权限)都取出来,而不是分开验证。所以,在Security提供的UserDetailsService默认实现JdbcDaoImpl中,角色和权限都存储在auhtorities表中。而不是像Shiro那样,角色有个roles表,权限有个permissions表。以及相关的管理表等
实现类:JaasGrantedAuthority、LdapAuthority、SimpleGrantedAuthority、SwitchUserGrantedAuthority
JaasGrantedAuthority GrantedAuthority除了分配的角色之外,还拥有主体,使用AuthorityGranter作为授予此权限的理由
SwitchUserGrantedAuthority 存储原始用户的身份验证对象,以便在以后从用户交换机“退出”时使用。
注意:在构建SimpleGrantedAuthority对象的时候,它没有添加任何前缀。所以表示“角色”的权限需要带有“ROLE_”前缀。Spring Security不管是角色、还是权限。它只比对字符串。所以角色需要加前缀。
角色信息存储的时候可以没有“ROLE_”前缀,包装成GrantedAuthority对象的时候必须要有
/**
* GrantedAuthority的基本具体实现。存储授予的权限的String(Authentication)
*/
public final class SimpleGrantedAuthority implements GrantedAuthority {
private final String role;
}
权限检查有两种方式:
①在配置类中,指定粗粒度的访问控制
②使用注解细粒度的控制访问
所有URL以"/admin"开头的用户必须拥有角色"ADMIN"才能访问。实际上操作的时候hasRole表达式,会判断参数是否包含"ROLE_"前缀,如果没有则加上去,然后再去校验。有这个前缀则直接校验。
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ADMIN')")
.antMatchers("/user/**").access("hasRole('USER')")
.anyRequest().authenticated();
}
注:需要使用注解@EnableGlobalMethodSecurity(prePostEnabled=true)开启
@PreAuthoritze("hasAuthority('readArtical')")
public List<Artical> getAll() {
//...
}
@PreAuthoritze注解,会从SecurityContext中取出Authencation对象,然后再取出Collection、 authorites集合。然后比对当前用户是否有权限"readArtical"。实际上就是比对集合中是否有那个GrantedAuthority的getAuthority()方法返回的字符串与"radArtical"匹配。
在任何@Configuration实例上使用@EnableGlobalMethodSecurity注解就能达到此目的。同时这个注解提供prePostEnabled、securedEnabled和jsr250Enabled三种不同的机制来实现同一种功能
prePostEnabled = true 会解锁@PreAuthorize和@PostAuthorize两个注解。从名字就可以看出@PreAuthorize注解会在方法执行前进行验证,而@PostAuthorize注解会在方法执行后进行验证。
public interface UserService {
List<User> findAllUsers();
@PostAuthorize ("returnObject.type == authentication.name")
User findById(int id);
// @PreAuthorize("hasRole('ADMIN')") 必须拥有ROLE_ADMIN角色。
@PreAuthorize("hasRole('ROLE_ADMIN ')")
void updateUser(User user);
@PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
void deleteUser(int id);
// @PreAuthorize("principal.username.startsWith('Felordcn')") 用户名开头为 Felordcn 的用户才能访问。
// @PreAuthorize("#id.equals(principal.username)") 入参 id 必须同当前的用户名相同。
// @PreAuthorize("#id < 10") 限制只能查询 id 小于 10 的用户
}
@PostAuthorize:
该注解使用不多,在方法执行后再进行权限验证。 适合验证带有返回值的权限。Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject。区别在于先执行方法。而后进行表达式判断。如果方法没有返回值实际上等于开放权限控制;如果有返回值实际的结果是用户操作成功但是得不到响应。允许方法调用,但是如果表达式计算结果为false,将抛出一个安全性异常。
@PreFilter:
对集合类型的参数执行过滤,移除结果为false的元素。基于方法入参相关的表达式,对入参进行过滤。分页慎用!该过程发生在接口接收参数之前。入参必须为 java.util.Collection且支持remove(Object)的参数。如果有多个集合需要通过 filterTarget=<参数名> 来指定过滤的集合。内置保留名称 filterObject 作为集合元素的操作名来进行评估过滤。
// 指定过滤的参数,过滤偶数
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> username)
@PostFilter:
和@PreFilter不同的是, 基于返回值相关的表达式,对返回值进行过滤。分页慎用!该过程发生接口进行数据返回之前
@Secured注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。
@Secured缺点(限制)就是不支持Spring EL表达式。不够灵活。并且指定的角色必须以ROLE_开头,不可省略。该注解功能要简单的多,默认情况下只能基于角色(默认需要带前缀 ROLE_)集合来进行访问控制决策。该注解的机制是只要其声明的角色集合(value)中包含当前用户持有的任一角色就可以访问。也就是 用户的角色集合和 @Secured 注解的角色集合要存在非空的交集。 不支持使用 SpEL 表达式进行决策。
@Secured({"ROLE_user"})
void updateUser(User user);
@Secured({"ROLE_admin", "ROLE_user1"})
void updateUser();
启用JSR-250安全控制注解,这属于JavaEE的安全规范(现为jakarta项目)。一共有五个安全注解。如果在@EnableGlobalMethodSecurity设置jsr250Enabled为true ,就开启了JavaEE 安全注解中的以下三个:
1.@DenyAll: 拒绝所有访问
2.@RolesAllowed({“USER”, “ADMIN”}): 该方法只要具有"USER", "ADMIN"任意一种权限就可以访问。这里可以省略前缀ROLE_,实际的权限可能是ROLE_ADMIN
3.@PermitAll: 允许所有访问
/*
* 配置没有权限自定义处理类
*
* 注意:hasRole(),如果出现异常,会调用设置的accessDeniedHandler方法。
* .antMatchers("/index").hasRole("ADMIN")
* .anyRequest().authenticated();
*/
httpSecurity.exceptionHandling()
.accessDeniedHandler(userAuthAccessDeniedHandler);
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。