前置知识:在IDEA上配置本地Sonar服务
这边简单说明一下如何操作:
下载时候要注意Sonar版本要与jdk相匹配,使用java -version查看自己的jdk版本(我这边是jdk17)所以我选择10.4.1版本的
下载解压后在目录内找到\bin\windows-x86-64里面有一个StartSonar.bat启动,没有报错后,在浏览器访问127.0.0.1:9000 进入管理(默认账号密码都是admin),并创建一个本地Local项目,跟着默认按就行了,这里就不演示了
打开idea项目,下载SonarLint插件,下载完要重启idea
之后打开setting/tools/sonarlint,配置项目(点击那个+号),只会这里的url就填你本地的访问路径
127.0.0.1:9000
之后创建token
选择允许
之后在项目根目录下面的pom文件放入下面这段代码(注意账号密码用自己的)
<profiles>
<profile>
<id>sonar</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<sonar.host.url>http://localhost:9000</sonar.host.url>
</properties>
</profile>
</profiles>
<properties>
<sonar.login>admin</sonar.login>
<sonar.password>admin</sonar.password>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>org.sonarsource.scanner.maven</groupId>
<artifactId>sonar-maven-plugin</artifactId>
<version>3.4.0.905</version>
</plugin>
</plugins>
</build>
添加完后重新加载pom
在右侧maven管理可以看到这个,允许即可生成
可能会需要你输入key,这个key是在Sonar上创建项目的时候输入的
1.使用SonarQube+人工检查,进行代码质量分析,从程序的语法、结构、接口等方面进行代码审查,并对代码风格进行分析。)
我们使用了SonarQube对微人事代码进行分析:
密码不应以纯文本形式存储:
代码:
简单修改:
个人感悟:事实上这个项目是有在 hrService 的认证逻辑中完成的密文保存逻辑的,不过这个插件只能静态分析,所以可以无法得出合适结论
2.构造函数问题
错误示例代码:
如何修正:
个人感悟:在原本的代码中,使用@Autowired注解直接注入了javaMailSender,。这种方式存在潜在的风险,因为在类初始化的时候,Spring框架可能还没有完成对依赖的注入,导致javaMailSender为null,从而在一些访问这个javaMailSender时抛出NullPointerException。
而修改后的代码中,通过构造函数注入javaMailSender,可以避免了潜在的NullPointerException风险。
Bean机制问题
错误示例代码:
修改后代码
个人感悟:通过在@Bean方法参数中注入DataSource依赖,使得依赖在方法内部被传递给MyService,更符合Spring的依赖注入机制,确保这个cachingConncetionFactory这个只能在这个bean里面被访问到
Switch问题
代码示例:
个人感悟:一般使用switch应该设置一个default来处理未考虑的情况
Java通配符问题
代码示例:
个人感悟:这个是Sonar给出的建议,一般来说我们使用这个T通配符是为了降低编码难度,但同时也会带来一定的风险,所以原则上不建议使用
Tip:在 Java 中,泛型类型的类型参数默认是不变的,因为它们可能出现在输入和输出位置的 同时。一个典型的例子是 接口。如果不是一成不变的,我们可以构造无效键入的情况。T get()add(T element)java.util.ListListT
在类型参数仅出现在一个位置的情况下,可以使用通配符来实现协方差或逆方差:
- <? extends Foo>协方差(输入位置)
- <? super Foo>对于逆方差(输出位置)
方法复杂度过高问题
代码示例:
个人感悟:这个方法的实现嵌套了三个for循环,时间复杂度飙升,使得整个项目的运行效率收到很大影响
代码规范问题:
个人感悟:有一部分的类命名不遵守驼峰,命名风格不统一
2.代码精读
RabbitConfig配置类:这段代码配置了 RabbitMQ 的基本组件,并在消息发送成功或失败时做出相应的处理,同时定义了消息队列、交换机和它们之间的绑定关系,确保消息能够正确地发送和路由到指定的队列中。这样的配置可以使系统能够与 RabbitMQ 进行交互,并实现消息的可靠投递和处理
1. rabbitTemplate
//通过该方法创建并配置了 RabbitTemplate 对象,用于在应用程序中与 RabbitMQ 进行交互。
@Bean
RabbitTemplate rabbitTemplate(CachingConnectionFactory cachingConnectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(cachingConnectionFactory);
//在 RabbitTemplate 中设置了消息发送成功的确认回调和消息发送失败的返回回调,用于处理消息发送结果。
rabbitTemplate.setConfirmCallback((data, ack, cause) -> {
String msgId = data.getId();
if (ack) {
logger.info(msgId + ":消息发送成功");
mailSendLogService.updateMailSendLogStatus(msgId, 1);//修改数据库中的记录,消息投递成功
} else {
logger.info(msgId + ":消息发送失败");
}
});
rabbitTemplate.setReturnCallback((msg, repCode, repText, exchange, routingkey) -> {
//当消息发送失败时,会打印日志信息。
logger.info("消息发送失败");
});
return rabbitTemplate;
}
2. mailQueue
//通过该方法创建了一个消息队列对象,用于接收发送的邮件消息。
@Bean
Queue mailQueue() {
return new Queue(MailConstants.MAIL_QUEUE_NAME, true);
}
3. mailExchange
//用于指定邮件消息的路由规则
@Bean
DirectExchange mailExchange() {
return new DirectExchange(MailConstants.MAIL_EXCHANGE_NAME, true, false);
}
4. mailBinding
//将消息队列和交换机绑定在一起,并指定了路由键
@Bean
Binding mailBinding() {
return BindingBuilder.bind(mailQueue()).to(mailExchange()).with(MailConstants.MAIL_ROUTING_KEY_NAME);
}
SecurityConfig安全配置(登录逻辑):此类用于配置安全相关的内容,并且实现了对用户登录认证、权限控制、会话管理等安全功能的配置,并且通过自定义的方式实现了一些特定的处理逻辑。
1. passwordEncoder
//加密密码
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
2. configure1
//指定了认证逻辑,这里使用了 HrService hrService 来处理用户认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService);
}
3. configure2
//配置了不需要经过安全过滤的静态资源路径
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/index.html", "/img/**", "/fonts/**", "/favicon.ico", "/verifyCode");
}
4. loginFilter
//登录成功和登录失败时的处理方式,并配置了并发会话控制策略
@Bean
LoginFilter loginFilter() throws Exception {
LoginFilter loginFilter = new LoginFilter();
loginFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
hr.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", hr);
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
);
loginFilter.setAuthenticationFailureHandler((request, response, exception) -> {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespBean respBean = RespBean.error(exception.getMessage());
if (exception instanceof LockedException) {
respBean.setMsg("账户被锁定,请联系管理员!");
} else if (exception instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期,请联系管理员!");
} else if (exception instanceof AccountExpiredException) {
respBean.setMsg("账户过期,请联系管理员!");
} else if (exception instanceof DisabledException) {
respBean.setMsg("账户被禁用,请联系管理员!");
} else if (exception instanceof BadCredentialsException) {
respBean.setMsg("用户名或者密码输入错误,请重新输入!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
);
loginFilter.setAuthenticationManager(authenticationManagerBean());
loginFilter.setFilterProcessesUrl("/doLogin");
ConcurrentSessionControlAuthenticationStrategy sessionStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
sessionStrategy.setMaximumSessions(1);
loginFilter.setSessionAuthenticationStrategy(sessionStrategy);
return loginFilter;
}
5. sessionRegistry
//管理会话信息
@Bean
SessionRegistryImpl sessionRegistry() {
return new SessionRegistryImpl();
}
6. configure3
//配置了请求的授权规则、退出登录处理、异常处理等
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setAccessDecisionManager(customUrlDecisionManager);
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
return object;
}
})
.and()
.logout()
.logoutSuccessHandler((req, resp, authentication) -> {
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
out.flush();
out.close();
}
)
.permitAll()
.and()
.csrf().disable().exceptionHandling()
//没有认证时,在这里处理结果,不要重定向
.authenticationEntryPoint((req, resp, authException) -> {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("访问失败!");
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
);
http.addFilterAt(new ConcurrentSessionFilter(sessionRegistry(), event -> {
HttpServletResponse resp = event.getResponse();
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.error("您已在另一台设备登录,本次登录已下线!")));
out.flush();
out.close();
}), ConcurrentSessionFilter.class);
http.addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class);
}
MailReceiver邮件接收类:实现了一个 RabbitMQ 消息监听器,用于接收消息并发送邮件。在处理消息时,会对消息进行去重判断,发送邮件并记录处理日志,确保消息的可靠处理和及时通知
1.handler
//这个注解指定了监听的消息队列为 MailConstants.MAIL_QUEUE_NAME
/*
当有消息到达时,会调用该方法进行处理
* message 代表接收到的消息内容
* channel 用于消息确认
* */
@RabbitListener(queues = MailConstants.MAIL_QUEUE_NAME)
public void handler(Message message, Channel channel) throws IOException {
Employee employee = (Employee) message.getPayload();
MessageHeaders headers = message.getHeaders();
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
String msgId = (String) headers.get("spring_returned_message_correlation");
if (redisTemplate.opsForHash().entries("mail_log").containsKey(msgId)) {
//redis 中包含该 key,说明该消息已经被消费过
logger.info(msgId + ":消息已经被消费");
channel.basicAck(tag, false);//确认消息已消费
return;
}
//收到消息,发送邮件
MimeMessage msg = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(msg);
try {
helper.setTo(employee.getEmail());
helper.setFrom(mailProperties.getUsername());
helper.setSubject("入职欢迎");
helper.setSentDate(new Date());
Context context = new Context();
context.setVariable("name", employee.getName());
context.setVariable("posName", employee.getPosition().getName());
context.setVariable("joblevelName", employee.getJobLevel().getName());
context.setVariable("departmentName", employee.getDepartment().getName());
String mail = templateEngine.process("mail", context);
helper.setText(mail, true);
javaMailSender.send(msg);
redisTemplate.opsForHash().put("mail_log", msgId, "javaboy");
channel.basicAck(tag, false);
logger.info(msgId + ":邮件发送成功");
} catch (MessagingException e) {
channel.basicNack(tag, false, true);
e.printStackTrace();
logger.error("邮件发送失败:" + e.getMessage());
}
}
LoginFilter登录过滤器 :实现了一个自定义的登录过滤器,用于处理用户登录认证请求。它支持处理 JSON 格式的登录请求,并在认证成功后注册新的会话,同时也提供了验证码的验证功能
//该过滤器用于处理用户登录认证的请求,并包含了一些自定义的逻辑
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
@Autowired
SessionRegistry sessionRegistry;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//判断请求方法是否为 POST,如果不是则抛出异常
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String verify_code = (String) request.getSession().getAttribute("verify_code");
//根据请求的 Content-Type 判断请求类型,如果是 JSON 格式的登录请求,则从请求体中读取用户名、密码和验证码,然后进行验证码校验
if (request.getContentType().contains(MediaType.APPLICATION_JSON_VALUE) || request.getContentType().contains(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
Map<String, String> loginData = new HashMap<>();
try {
loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
} catch (IOException e) {
}finally {
String code = loginData.get("code");
checkCode(response, code, verify_code);
}
String username = loginData.get(getUsernameParameter());
String password = loginData.get(getPasswordParameter());
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
Hr principal = new Hr();
principal.setUsername(username);
sessionRegistry.registerNewSession(request.getSession(true).getId(), principal);
//调用 getAuthenticationManager().authenticate(authRequest) 方法进行认证。
return this.getAuthenticationManager().authenticate(authRequest);
} else {
checkCode(response, request.getParameter("code"), verify_code);
return super.attemptAuthentication(request, response);
}
}
//验证用户输入的验证码是否正确
public void checkCode(HttpServletResponse resp, String code, String verify_code) {
if (code == null || verify_code == null || "".equals(code) || !verify_code.toLowerCase().equals(code.toLowerCase())) {
//验证码不正确
throw new AuthenticationServiceException("验证码不正确");
}
}
}
CustomFilterInvocationSecurityMetadataSource 权限过滤:自定义安全元数据源用于动态地根据请求的 URL 获取所需的安全配置属性,以便进行访问控制和权限验证。这样可以灵活地根据菜单和角色配置来控制系统的访问权限
1. getAttributes
//匹配请求的 URL 和菜单的 URL。如果匹配成功,则将该菜单所需的角色转换为 ConfigAttribute 集合返回。
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) object).getRequestUrl();
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
List<Role> roles = menu.getRoles();
String[] str = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
str[i] = roles.get(i).getName();
}
//匹配请求的 URL 和菜单的 URL。如果匹配成功,则将该菜单所需的角色转换为 ConfigAttribute 集合返回。
return SecurityConfig.createList(str);
}
}
//如果未匹配到任何菜单,或者菜单列表为空,将返回一个默认的 ROLE_LOGIN,表示需要登录才能访问。
return SecurityConfig.createList("ROLE_LOGIN");
}
2. getAllConfigAttributes
//返回了所有定义好的安全配置属性
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
3. supports
//指示了该安全元数据源是否支持给定的安全对象类型,true表示支持所有类型的安全对象
@Override
public boolean supports(Class<?> clazz) {
return true;
}
Router.js 路由分配:
export default new Router({
// 定义路由表
routes: [
{
path: '/',
name: 'Login',
component: Login,
hidden: true
}, {
path: '/home',
name: 'Home',
component: Home,
hidden: true,
meta: {
roles: ['admin', 'user'] // 设置访问该路由需要的角色为 admin 或 user
},
children: [
{
path: '/chat',
name: '在线聊天',
component: FriendChat,
hidden: true
}, {
path: '/hrinfo',
name: '个人中心',
component: HrInfo,
hidden: true //
}
]
}, {
path: '*', // 匹配任意路径
redirect: '/home' // 重定向到 /home 路由
}
]
})
Api.js 请求api重新封装:
// axios 响应拦截器
axios.interceptors.response.use(success => {
if (success.status && success.status == 200 && success.data.status == 500) {
Message.error({message: success.data.msg})
return;
}
if (success.data.msg) {
Message.success({message: success.data.msg})
}
return success.data;
}, error => {
//服务器错误码处理
if (error.response.status == 504 || error.response.status == 404) {
Message.error({message: '服务器被吃了( ╯□╰ )'})
} else if (error.response.status == 403) {
Message.error({message: '权限不足,请联系管理员'})
} else if (error.response.status == 401) {
mymessage.error({message: error.response.data.msg ? error.response.data.msg : '尚未登录,请登录'})
router.replace('/');
} else {
if (error.response.data.msg) {
Message.error({message: error.response.data.msg})
} else {
Message.error({message: '未知错误!'})
}
}
return;
})
let base = '';
// 发送键值对格式的 POST 请求
export const postKeyValueRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = '';
for (let i in data) {
ret += encodeURIComponent(i) + '=' + encodeURIComponent(data[i]) + '&'
}
return ret;
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
}
// 发送 POST 请求
export const postRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params
})
}
// 发送 PUT 请求
export const putRequest = (url, params) => {
return axios({
method: 'put',
url: `${base}${url}`,
data: params
})
}
// 发送 GET 请求
export const getRequest = (url, params) => {
return axios({
method: 'get',
url: `${base}${url}`,
params: params
})
}
// 发送 DELETE 请求
export const deleteRequest = (url, params) => {
return axios({
method: 'delete',
url: `${base}${url}`,
params: params
})
}
Login.vue 登录基本页面ui设计:
- template模板
其中<el-***>标签是来自于Element-ui前端组件提供的标签,通过这个标签来实现一个基本的模板
<template>
<div>
<el-form
:rules="rules"
ref="loginForm"
v-loading="loading"
element-loading-text="正在登录..."
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.8)"
:model="loginForm"
class="loginContainer">
<h3 class="loginTitle">系统登录</h3>
<el-form-item prop="username">
<el-input size="normal" type="text" v-model="loginForm.username" auto-complete="off"
placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input size="normal" type="password" v-model="loginForm.password" auto-complete="off"
placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item prop="code">
<el-input size="normal" type="text" v-model="loginForm.code" auto-complete="off"
placeholder="点击图片更换验证码" @keydown.enter.native="submitLogin" style="width: 250px"></el-input>
<img :src="vcUrl" @click="updateVerifyCode" alt="" style="cursor: pointer">
</el-form-item>
<el-checkbox size="normal" class="loginRemember" v-model="checked"></el-checkbox>
<el-button size="normal" type="primary" style="width: 100%;" @click="submitLogin">登录</el-button>
</el-form>
</div>
</template>
- script脚本
通过这个script定义了表单验证规则。在 data 方法中定义了表单的初始数据,包括 loading 状态、验证码的 URL、登录表单的数据以及验证规则。
两个方法:
updateVerifyCode:用于更新验证码的 URL,以便刷新验证码图片。
submitLogin:用于提交登录表单的方法。在方法内部首先进行表单验证,然后通过 postRequest 方法发送登录请求。根据响应结果来更新页面状态和处理登录成功与失败的逻辑。
<script>
export default {
name: "Login",
data() {
return {
loading: false,
vcUrl: '/verifyCode?time='+new Date(),
loginForm: {
username: 'admin',
password: '123',
code:''
},
checked: true,
rules: {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}],
code: [{required: true, message: '请输入验证码', trigger: 'blur'}]
}
}
},
methods: {
updateVerifyCode() {
this.vcUrl = '/verifyCode?time='+new Date();
},
submitLogin() {
this.$refs.loginForm.validate((valid) => {
if (valid) {
this.loading = true;
this.postRequest('/doLogin', this.loginForm).then(resp => {
this.loading = false;
if (resp) {
this.$store.commit('INIT_CURRENTHR', resp.obj);
window.sessionStorage.setItem("user", JSON.stringify(resp.obj));
let path = this.$route.query.redirect;
this.$router.replace((path == '/' || path == undefined) ? '/home' : path);
}else{
this.vcUrl = '/verifyCode?time='+new Date();
}
})
} else {
return false;
}
});
}
}
}
</script>
- style 风格
基本的css,通过id选择器选择一些标签,并通过设置一些常见的属性来美化template模板
<style>
.loginContainer {
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 15px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.loginTitle {
margin: 15px auto 20px auto;
text-align: center;
color: #505458;
}
.loginRemember {
text-align: left;
margin: 0px 0px 15px 0px;
}
.el-form-item__content{
display: flex;
align-items: center;
}
</style>